Test Component React
Test React components with Testing Library using user-centric queries and async utilities
When to Use
- Testing React component rendering, interaction, and state changes
- Verifying that components display the correct content
- Simulating user interactions (clicks, typing, form submission)
- Testing async components that fetch data or show loading states
Instructions
- Render and query — use
renderandscreen:
import { render, screen } from '@testing-library/react';
import { UserCard } from './user-card';
it('displays the user name', () => {
render(<UserCard user={{ name: 'Alice', email: 'alice@test.com' }} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('alice@test.com')).toBeInTheDocument();
});
- Use accessible queries in priority order:
getByRole— buttons, links, headings, form elementsgetByLabelText— form inputs by labelgetByPlaceholderText— inputs by placeholdergetByText— visible text contentgetByTestId— last resort, for elements without accessible names
it('renders a submit button', () => {
render(<LoginForm />);
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
expect(screen.getByLabelText('Email')).toBeInTheDocument();
});
- Simulate user interactions with
userEvent:
import userEvent from '@testing-library/user-event';
it('submits the form with entered data', async () => {
const
render(<LoginForm />);
const user = userEvent.setup();
await user.type(screen.getByLabelText('Email'), 'alice@test.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Log in' }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'alice@test.com',
password: 'secret123',
});
});
- Test async behavior with
waitForandfindBy:
it('shows user data after loading', async () => {
render(<UserProfile userId="123" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
// findBy waits for the element to appear (default 1000ms timeout)
expect(await screen.findByText('Alice')).toBeInTheDocument();
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
- Test conditional rendering:
it('shows error message for invalid input', async () => {
render(<EmailInput />);
const user = userEvent.setup();
await user.type(screen.getByLabelText('Email'), 'not-an-email');
await user.tab(); // Trigger blur validation
expect(screen.getByText('Please enter a valid email')).toBeInTheDocument();
});
- Verify absence with
queryBy(returns null instead of throwing):
it('does not show admin panel for regular users', () => {
render(<Dashboard user={{ role: 'user' }} />);
expect(screen.queryByText('Admin Panel')).not.toBeInTheDocument();
});
- Test with context providers:
function renderWithProviders(ui: React.ReactElement) {
return render(
<ThemeProvider theme={defaultTheme}>
<AuthProvider value={mockAuth}>
{ui}
</AuthProvider>
</ThemeProvider>
);
}
it('uses theme colors', () => {
renderWithProviders(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveStyle({ color: 'blue' });
});
- Prefer
userEventoverfireEvent—userEventsimulates real browser behavior (focus, keyboard events, click sequence):
// Good — fires focus, keydown, keypress, input, keyup for each character
await user.type(input, 'hello');
// Less realistic — fires only the change event
fireEvent.change(input, { target: { value: 'hello' } });
Details
Testing Library's philosophy is "test the way users interact with your app." Tests should not know about component internals (state, props, hooks) — they should only interact through the rendered DOM.
Query types:
getBy— returns element or throws. Use when the element must be presentqueryBy— returns element or null. Use when asserting absencefindBy— returns a Promise. Use for elements that appear asynchronously- All have
AllByvariants that return arrays
userEvent.setup(): Always call userEvent.setup() before interactions. This creates a user session with proper event sequencing. Do not use the older userEvent.click() static methods.
jsdom limitations: Testing Library runs in jsdom, which does not implement layout. getComputedStyle, getBoundingClientRect, and scroll behavior do not work. Use Playwright for visual and layout testing.
Trade-offs:
- User-centric testing avoids testing implementation details — but some internal states are hard to observe through the DOM
getByRoleencourages accessible markup — but can be frustrating when role names are not obvioususerEventis realistic — but slower thanfireEvent. UsefireEventfor simple cases in large test suiteswaitForandfindByhandle async — but can mask slow components. Set explicit timeouts in CI
Source
https://testing-library.com/docs/react-testing-library/intro/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.