Comprehensive testing guidelines for React 19 applications with TypeScript
Priority High:
Priority Medium:
Avoid Testing:
1. Accessible Queries (Most Preferred):
getByRole - Most preferredgetByLabelText - For form elementsgetByPlaceholderText - Alternative for inputsgetByText - For non-interactive elementsgetByDisplayValue - For current input values2. Semantic Queries:
getByAltText - For imagesgetByTitle - For title attributes3. Test IDs (Last Resort):
getByTestId - Only when element has no accessible role| Variant | Behavior |
|---|---|
getBy |
Throws error if not found - for elements that must exist |
queryBy |
Returns null if not found - for asserting non-existence |
findBy |
Returns promise - for async elements that appear later |
Library: Use @testing-library/user-event, not fireEvent
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('user can type in input', async () => {
const user = userEvent.setup();
render(<SearchBox />);
const input = screen.getByRole('textbox');
await user.type(input, 'Hello');
expect(input).toHaveValue('Hello');
});
Common Interactions:
user.click() - Click elementsuser.type() - Type in inputsuser.clear() - Clear input valuesuser.selectOptions() - Select dropdown optionsuser.upload() - Upload filesimport { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ComponentName } from './ComponentName';
describe('ComponentName', () => {
test('describes expected behavior', async () => {
// Arrange - Set up test data and render
const user = userEvent.setup();
render(<ComponentName prop="value" />);
// Act - Perform user interactions
const button = screen.getByRole('button', { name: /click me/i });
await user.click(button);
// Assert - Verify expected outcomes
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
});
Basic:
test('renders with correct props', () => {
render(<UserCard name="John" email="john@example.com" />);
expect(screen.getByText('John')).toBeInTheDocument();
expect(screen.getByText('john@example.com')).toBeInTheDocument();
});
Conditional:
test('shows loading state', () => {
render(<DataDisplay isLoading={true} />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
Button Click:
test('increments counter on click', 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();
});
Form Submission:
test('submits form with user data', async () => {
const handleSubmit = jest.fn();
const user = userEvent.setup();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'user@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /submit/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'user@example.com',
password: 'password123',
});
});
Data Fetching:
test('displays fetched data', async () => {
render(<UserList />);
// Wait for loading to finish
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for data to appear
const users = await screen.findAllByRole('listitem');
expect(users).toHaveLength(3);
});
With waitFor:
test('shows success message after submission', async () => {
const user = userEvent.setup();
render(<ContactForm />);
await user.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(screen.getByText(/thank you/i)).toBeInTheDocument();
});
});
test('displays error message on failure', async () => {
// Mock API to return error
jest.spyOn(api, 'fetchUser').mockRejectedValue(new Error('Failed'));
render(<UserProfile userId="123" />);
const errorMessage = await screen.findByText(/failed to load/i);
expect(errorMessage).toBeInTheDocument();
});
API Calls:
// Mock API module
jest.mock('@/lib/api', () => ({
fetchUsers: jest.fn(),
}));
test('renders users from API', async () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
fetchUsers.mockResolvedValue(mockUsers);
render(<UserList />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
});
React Query:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function renderWithQueryClient(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>
);
}
Avoid:
Acceptable:
test('button is keyboard accessible', async () => {
const user = userEvent.setup();
render(<Dialog />);
// Tab to button
await user.tab();
expect(screen.getByRole('button')).toHaveFocus();
// Activate with Enter
await user.keyboard('{Enter}');
expect(screen.getByRole('dialog')).toBeInTheDocument();
});
ComponentName.test.tsx - Component testsutils.test.ts - Utility function tests__tests__/ directory - Alternative structuredescribe('LoginForm', () => {
describe('validation', () => {
test('shows error for invalid email', () => {});
test('shows error for short password', () => {});
});
describe('submission', () => {
test('calls onSubmit with form data', () => {});
test('shows success message after submit', () => {});
});
});
| Area | Coverage |
|---|---|
| Critical paths | 100% |
| Components | 80%+ |
| Utilities | 90%+ |