From aai-stack-react
Provides React Testing Library patterns for component testing: rendering with providers, preferred queries by role/label/text, role-based selectors, and user interactions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/aai-stack-react:react-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Patterns for testing React components with React Testing Library.
Patterns for testing React components with React Testing Library.
React Testing Library encourages testing components the way users interact with them, not implementation details.
import { render, screen } from '@testing-library/react'
test('renders greeting', () => {
render(<Greeting name="World" />)
expect(screen.getByText('Hello, World!')).toBeInTheDocument()
})
function renderWithProviders(
ui: ReactElement,
{
preloadedState = {},
...renderOptions
} = {}
) {
function Wrapper({ children }: { children: ReactNode }) {
return (
<Provider store={setupStore(preloadedState)}>
<ThemeProvider>
<Router>
{children}
</Router>
</ThemeProvider>
</Provider>
)
}
return render(ui, { wrapper: Wrapper, ...renderOptions })
}
// Usage
test('renders with providers', () => {
renderWithProviders(<UserDashboard />)
expect(screen.getByText('Dashboard')).toBeInTheDocument()
})
Use queries in this order (most to least preferred):
// 1. Accessible queries (preferred)
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText('Email')
screen.getByPlaceholderText('Enter email')
screen.getByText('Hello World')
screen.getByDisplayValue('current value')
// 2. Semantic queries
screen.getByAltText('Profile picture')
screen.getByTitle('Close')
// 3. Test IDs (last resort)
screen.getByTestId('custom-element')
// getBy* - Throws if not found (use for elements that should exist)
screen.getByRole('button')
// queryBy* - Returns null if not found (use for asserting absence)
expect(screen.queryByText('Loading')).not.toBeInTheDocument()
// findBy* - Returns promise, waits for element (use for async)
const button = await screen.findByRole('button')
// *AllBy* - Returns array of all matches
const items = screen.getAllByRole('listitem')
// Buttons
screen.getByRole('button', { name: /submit/i })
// Links
screen.getByRole('link', { name: /home/i })
// Form elements
screen.getByRole('textbox', { name: /email/i })
screen.getByRole('checkbox', { name: /remember me/i })
screen.getByRole('combobox', { name: /country/i })
// Headings
screen.getByRole('heading', { name: /welcome/i, level: 1 })
// Navigation
screen.getByRole('navigation')
// Lists
screen.getByRole('list')
screen.getAllByRole('listitem')
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
test('button click increments counter', async () => {
const user = userEvent.setup()
render(<Counter />)
const button = screen.getByRole('button', { name: /increment/i })
await user.click(button)
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
test('form submission', async () => {
const user = userEvent.setup()
const onSubmit = jest.fn()
render(<LoginForm onSubmit={onSubmit} />)
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /sign in/i }))
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123',
})
})
test('select option', async () => {
const user = userEvent.setup()
render(<CountrySelect />)
await user.selectOptions(
screen.getByRole('combobox', { name: /country/i }),
'US'
)
expect(screen.getByRole('combobox')).toHaveValue('US')
})
test('keyboard navigation', async () => {
const user = userEvent.setup()
render(<Form />)
await user.tab() // Focus first input
await user.type(screen.getByLabelText(/name/i), 'John')
await user.tab() // Focus next input
await user.keyboard('{Enter}') // Submit form
})
test('loads user data', async () => {
render(<UserProfile userId="123" />)
// Wait for loading to finish
expect(screen.getByText(/loading/i)).toBeInTheDocument()
// Wait for data to appear
expect(await screen.findByText('John Doe')).toBeInTheDocument()
})
import { waitFor } from '@testing-library/react'
test('form validation', async () => {
const user = userEvent.setup()
render(<Form />)
await user.click(screen.getByRole('button', { name: /submit/i }))
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument()
})
})
test('disappearing element', async () => {
render(<Notification />)
expect(screen.getByText(/success/i)).toBeInTheDocument()
await waitFor(() => {
expect(screen.queryByText(/success/i)).not.toBeInTheDocument()
})
})
test('loading state disappears', async () => {
render(<DataLoader />)
await waitForElementToBeRemoved(() => screen.queryByText(/loading/i))
expect(screen.getByText('Data loaded')).toBeInTheDocument()
})
import { rest } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
rest.get('/api/user/:id', (req, res, ctx) => {
return res(ctx.json({ id: req.params.id, name: 'John Doe' }))
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('fetches user data', async () => {
render(<UserProfile userId="123" />)
expect(await screen.findByText('John Doe')).toBeInTheDocument()
})
test('handles error', async () => {
server.use(
rest.get('/api/user/:id', (req, res, ctx) => {
return res(ctx.status(500))
})
)
render(<UserProfile userId="123" />)
expect(await screen.findByText(/error loading user/i)).toBeInTheDocument()
})
jest.mock('../hooks/useUser', () => ({
useUser: jest.fn(),
}))
import { useUser } from '../hooks/useUser'
test('renders user when loaded', () => {
(useUser as jest.Mock).mockReturnValue({
user: { name: 'John' },
loading: false,
error: null,
})
render(<UserProfile />)
expect(screen.getByText('John')).toBeInTheDocument()
})
import { MemoryRouter, Route, Routes } from 'react-router-dom'
test('navigates on click', async () => {
const user = userEvent.setup()
render(
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</MemoryRouter>
)
await user.click(screen.getByRole('link', { name: /about/i }))
expect(screen.getByText(/about page/i)).toBeInTheDocument()
})
describe('RegistrationForm', () => {
test('shows validation errors for empty required fields', async () => {
const user = userEvent.setup()
render(<RegistrationForm />)
await user.click(screen.getByRole('button', { name: /register/i }))
expect(await screen.findByText(/name is required/i)).toBeInTheDocument()
expect(screen.getByText(/email is required/i)).toBeInTheDocument()
})
test('submits with valid data', async () => {
const user = userEvent.setup()
const onSubmit = jest.fn()
render(<RegistrationForm onSubmit={onSubmit} />)
await user.type(screen.getByLabelText(/name/i), 'John Doe')
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.click(screen.getByRole('button', { name: /register/i }))
expect(onSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: '[email protected]',
})
})
})
test('opens and closes modal', async () => {
const user = userEvent.setup()
render(<App />)
// Modal not visible initially
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
// Open modal
await user.click(screen.getByRole('button', { name: /open modal/i }))
expect(screen.getByRole('dialog')).toBeInTheDocument()
// Close modal
await user.click(screen.getByRole('button', { name: /close/i }))
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'))
})
test('renders and filters list items', async () => {
const user = userEvent.setup()
render(<TodoList />)
// Verify initial items
const items = screen.getAllByRole('listitem')
expect(items).toHaveLength(3)
// Filter items
await user.type(screen.getByRole('searchbox'), 'buy')
const filteredItems = screen.getAllByRole('listitem')
expect(filteredItems).toHaveLength(1)
expect(filteredItems[0]).toHaveTextContent('Buy groceries')
})
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
test('has no accessibility violations', async () => {
const { container } = render(<MyComponent />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
test('debugging', () => {
render(<Component />)
// Print current DOM
screen.debug()
// Print specific element
screen.debug(screen.getByRole('button'))
// Log accessible roles
screen.logTestingPlaygroundURL() // Opens Testing Playground with current DOM
})
Used by:
frontend-developer agente2e-test-specialist agentunit-test-specialist agentnpx claudepluginhub bradtaylorsf/alphaagent-teamProvides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.