Render, Screen & Queries
React Testing Library: Render, Screen & Queries RTL encourages testing components the way users interact with them — through accessibility roles, labels, and vi…
React Testing Library: Render, Screen & Queries
RTL encourages testing components the way users interact with them — through accessibility roles, labels, and visible text — rather than testing implementation details like component state or internal methods.
Setup & render
// Install
// npm install --save-dev @testing-library/react @testing-library/user-event @testing-library/jest-dom
// npm install --save-dev vitest jsdom @vitejs/plugin-react (Vite projects)
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: { environment: 'jsdom', globals: true, setupFiles: './src/test/setup.ts' },
})
// src/test/setup.ts
import '@testing-library/jest-dom'
// Basic render
import { render, screen } from '@testing-library/react'
import { Button } from './Button'
test('renders button with label', () => {
render(<Button onClick={jest.fn()}>Save</Button>)
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument()
})Query Types & Priority
RTL has three query variants × multiple query strategies. Always prefer the most accessible query (role > label > text > testId).
Query variants:
getBy* — throws if not found or multiple found. Use for asserting element exists.
queryBy* — returns null if not found. Use to assert element does NOT exist.
findBy* — async, returns Promise. Use when element appears after async operation.
getAllBy* / queryAllBy* / findAllBy* — same but for multiple elements.
Query strategies (priority order — use highest available):
ByRole getByRole('button', { name: /save/i }) — most preferred (accessibility)
ByLabelText getByLabelText('Email address') — form inputs
ByPlaceholderText getByPlaceholderText('Search...')
ByText getByText(/hello world/i) — non-interactive elements
ByDisplayValue getByDisplayValue('Alice') — selected input value
ByAltText getByAltText('Profile photo') — images
ByTitle getByTitle('Close')
ByTestId getByTestId('submit-btn') — last resort// ByRole — most powerful query
// ARIA roles: button, link, heading, textbox, checkbox, radio,
// combobox, listbox, option, img, dialog, alert, navigation, main
screen.getByRole('heading', { level: 1 }) // <h1>
screen.getByRole('textbox', { name: /email/i }) // <input> with label "Email"
screen.getByRole('button', { name: /submit/i })
screen.getByRole('checkbox', { name: /agree/i, checked: true })
screen.getByRole('combobox', { name: /country/i }) // <select>
// ByLabelText — recommended for form inputs
screen.getByLabelText('Email address')
screen.getByLabelText(/password/i)
// Works with: <label for>, aria-label, aria-labelledby, title
// ByText — for static content
screen.getByText('Hello, World!')
screen.getByText(/hello/i) // regex — case insensitive
screen.getByText('Price:', { exact: false }) // substring match
// queryBy — assert absence
expect(screen.queryByText('Error message')).not.toBeInTheDocument()
// getAllBy — multiple elements
const items = screen.getAllByRole('listitem')
expect(items).toHaveLength(3)jest-dom Matchers
// @testing-library/jest-dom extends expect() with DOM-specific matchers
expect(element).toBeInTheDocument()
expect(element).not.toBeInTheDocument()
expect(element).toBeVisible()
expect(element).toBeDisabled()
expect(element).toBeEnabled()
expect(element).toBeChecked()
expect(element).toHaveFocus()
expect(element).toHaveValue('alice@example.com')
expect(element).toHaveDisplayValue('Option A')
expect(element).toHaveTextContent(/hello/i)
expect(element).toHaveTextContent('exact string')
expect(element).toHaveAttribute('aria-label', 'Close')
expect(element).toHaveAttribute('disabled')
expect(element).toHaveClass('btn-primary')
expect(element).toHaveStyle({ backgroundColor: 'red' })