Loading...
Loading...
Comprehensive React component testing with Jest and React Testing Library covering configuration, mocking strategies, async testing patterns, hooks testing, and integration testing best practices
npx skill4agent add manutej/luxor-claude-marketplace jest-react-testing/** @type {import('jest').Config} */
const config = {
// Test environment for DOM testing
testEnvironment: 'jsdom',
// Setup files after environment
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// Module paths
moduleDirectories: ['node_modules', 'src'],
// Transform files with babel-jest
transform: {
'^.+\\.(js|jsx)$': 'babel-jest',
},
// Module name mapper for static assets and CSS
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
},
// Coverage configuration
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/**/*.test.{js,jsx}',
'!src/**/__tests__/**',
],
// Coverage thresholds
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
module.exports = config;import type {Config} from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
moduleDirectories: ['node_modules', 'src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.ts',
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/index.tsx',
'!src/**/*.test.{ts,tsx}',
'!src/**/__tests__/**',
'!src/**/*.d.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
export default config;// Add custom jest matchers from jest-dom
import '@testing-library/jest-dom';
// Extend expect with jest-extended matchers (optional)
import * as matchers from 'jest-extended';
expect.extend(matchers);
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
};
// Suppress console errors in tests (optional)
const originalError = console.error;
beforeAll(() => {
console.error = (...args) => {
if (
typeof args[0] === 'string' &&
args[0].includes('Warning: ReactDOM.render')
) {
return;
}
originalError.call(console, ...args);
};
});
afterAll(() => {
console.error = originalError;
});
// Reset mocks after each test
afterEach(() => {
jest.clearAllMocks();
});module.exports = 'test-file-stub';module.exports = {};getByRole('button', { name: /submit/i })
getByRole('heading', { level: 1 })
getByRole('textbox', { name: /username/i })getByLabelText(/email address/i)
getByLabelText('Password')getByPlaceholderText(/search/i)getByText(/welcome/i)
getByText('Error: Invalid credentials')getByDisplayValue('John Doe')getByAltText(/profile picture/i)getByTitle(/close/i)getByTestId('custom-element')// Single element queries
screen.getByRole('button') // Throws if not found or multiple found
screen.queryByRole('button') // Returns null if not found
await screen.findByRole('button') // Async, waits up to 1000ms
// Multiple element queries
screen.getAllByRole('listitem') // Throws if none found
screen.queryAllByRole('listitem') // Returns [] if none found
await screen.findAllByRole('listitem') // Async versionimport { render, screen } from '@testing-library/react';
import { Greeting } from './Greeting';
describe('Greeting Component', () => {
it('renders greeting message', () => {
render(<Greeting name="Alice" />);
expect(screen.getByText(/hello, alice/i)).toBeInTheDocument();
});
it('renders default greeting when no name provided', () => {
render(<Greeting />);
expect(screen.getByText(/hello, guest/i)).toBeInTheDocument();
});
});import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter Component', () => {
it('increments counter on button click', async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
const count = screen.getByText(/count: 0/i);
expect(count).toBeInTheDocument();
await user.click(button);
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
it('decrements counter on button click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={5} />);
const decrementBtn = screen.getByRole('button', { name: /decrement/i });
await user.click(decrementBtn);
expect(screen.getByText(/count: 4/i)).toBeInTheDocument();
});
});import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm Component', () => {
it('submits form with username and password', async () => {
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const usernameInput = screen.getByLabelText(/username/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /submit/i });
await user.type(usernameInput, 'testuser');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalledTimes(1);
expect(handleSubmit).toHaveBeenCalledWith({
username: 'testuser',
password: 'password123',
});
});
it('shows validation errors for empty fields', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={jest.fn()} />);
const submitButton = screen.getByRole('button', { name: /submit/i });
await user.click(submitButton);
expect(screen.getByText(/username is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
});import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
describe('UserProfile Component', () => {
it('shows loading state when loading', () => {
render(<UserProfile loading={true} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
});
it('shows user data when loaded', () => {
const user = {
name: 'John Doe',
email: 'john@example.com',
};
render(<UserProfile loading={false} user={user} />);
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
expect(screen.getByRole('heading', { name: /john doe/i })).toBeInTheDocument();
expect(screen.getByText(/john@example.com/i)).toBeInTheDocument();
});
it('shows error message when error occurs', () => {
render(<UserProfile loading={false} error="Failed to load user" />);
expect(screen.getByText(/failed to load user/i)).toBeInTheDocument();
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
});
});// __mocks__/axios.js
export default {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
};import axios from 'axios';
import { UserService } from './UserService';
jest.mock('axios');
describe('UserService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('fetches users successfully', async () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
axios.get.mockResolvedValue({ data: mockUsers });
const users = await UserService.getUsers();
expect(axios.get).toHaveBeenCalledWith('/api/users');
expect(users).toEqual(mockUsers);
});
it('handles fetch error', async () => {
axios.get.mockRejectedValue(new Error('Network Error'));
await expect(UserService.getUsers()).rejects.toThrow('Network Error');
});
});import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button Component', () => {
it('calls onClick handler when clicked', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('calls onClick with event object', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click Me</Button>);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledWith(
expect.objectContaining({
type: 'click',
})
);
});
});// src/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
])
);
}),
rest.post('/api/users', async (req, res, ctx) => {
const { name, email } = await req.json();
return res(
ctx.status(201),
ctx.json({
id: 3,
name,
email,
})
);
}),
];// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());import { render, screen, waitFor } from '@testing-library/react';
import { server } from './mocks/server';
import { rest } from 'msw';
import { UserList } from './UserList';
describe('UserList Component', () => {
it('fetches and displays users', async () => {
render(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/alice/i)).toBeInTheDocument();
expect(screen.getByText(/bob/i)).toBeInTheDocument();
});
});
it('handles server error', async () => {
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.status(500),
ctx.json({ message: 'Internal Server Error' })
);
})
);
render(<UserList />);
await waitFor(() => {
expect(screen.getByText(/error loading users/i)).toBeInTheDocument();
});
});
});import { render, screen } from '@testing-library/react';
import { AuthContext } from './AuthContext';
import { ProtectedComponent } from './ProtectedComponent';
const mockAuthContext = (overrides = {}) => ({
user: { id: 1, name: 'Test User' },
isAuthenticated: true,
login: jest.fn(),
logout: jest.fn(),
...overrides,
});
describe('ProtectedComponent', () => {
it('renders content for authenticated user', () => {
const contextValue = mockAuthContext();
render(
<AuthContext.Provider value={contextValue}>
<ProtectedComponent />
</AuthContext.Provider>
);
expect(screen.getByText(/welcome, test user/i)).toBeInTheDocument();
});
it('renders login prompt for unauthenticated user', () => {
const contextValue = mockAuthContext({
user: null,
isAuthenticated: false,
});
render(
<AuthContext.Provider value={contextValue}>
<ProtectedComponent />
</AuthContext.Provider>
);
expect(screen.getByText(/please log in/i)).toBeInTheDocument();
});
});import { render, screen } from '@testing-library/react';
import { ParentComponent } from './ParentComponent';
// Mock the child component
jest.mock('./ChildComponent', () => ({
ChildComponent: ({ title, onAction }) => (
<div>
<h2>{title}</h2>
<button onClick={onAction}>Mocked Action</button>
</div>
),
}));
describe('ParentComponent', () => {
it('renders with mocked child', () => {
render(<ParentComponent />);
expect(screen.getByText(/mocked action/i)).toBeInTheDocument();
});
});import { render, screen, waitFor } from '@testing-library/react';
import { AsyncComponent } from './AsyncComponent';
describe('AsyncComponent', () => {
it('loads and displays data', async () => {
render(<AsyncComponent />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/data loaded/i)).toBeInTheDocument();
});
});
it('waits for specific condition', async () => {
render(<AsyncComponent />);
await waitFor(
() => {
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
},
{ timeout: 3000 }
);
});
});import { render, screen } from '@testing-library/react';
import { DataFetcher } from './DataFetcher';
describe('DataFetcher Component', () => {
it('displays fetched data', async () => {
render(<DataFetcher />);
// findBy automatically waits for element to appear
const heading = await screen.findByRole('heading', { name: /data/i });
expect(heading).toBeInTheDocument();
});
it('handles timeout for missing elements', async () => {
render(<DataFetcher url="/api/missing" />);
await expect(
screen.findByText(/success/i, {}, { timeout: 500 })
).rejects.toThrow();
});
});import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AsyncForm } from './AsyncForm';
describe('AsyncForm Component', () => {
it('submits form and shows success message', async () => {
const user = userEvent.setup();
render(<AsyncForm />);
const input = screen.getByLabelText(/name/i);
const submitBtn = screen.getByRole('button', { name: /submit/i });
await user.type(input, 'John Doe');
await user.click(submitBtn);
const successMsg = await screen.findByText(/submitted successfully/i);
expect(successMsg).toBeInTheDocument();
});
it('shows error message on failure', async () => {
const user = userEvent.setup();
render(<AsyncForm shouldFail={true} />);
const submitBtn = screen.getByRole('button', { name: /submit/i });
await user.click(submitBtn);
const errorMsg = await screen.findByRole('alert');
expect(errorMsg).toHaveTextContent(/submission failed/i);
});
});import { render, screen, act } from '@testing-library/react';
import { Timer } from './Timer';
describe('Timer Component', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('updates timer every second', () => {
render(<Timer />);
expect(screen.getByText(/0 seconds/i)).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.getByText(/1 second/i)).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(3000);
});
expect(screen.getByText(/4 seconds/i)).toBeInTheDocument();
});
it('cleans up timer on unmount', () => {
const { unmount } = render(<Timer />);
const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
unmount();
expect(clearIntervalSpy).toHaveBeenCalled();
});
});import { renderHook } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter Hook', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('initializes with provided value', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('decrements count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(4);
});
});import { renderHook } from '@testing-library/react';
import { useFetch } from './useFetch';
describe('useFetch Hook', () => {
it('fetches data for given URL', async () => {
const { result } = renderHook(() => useFetch('/api/users'));
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toBeDefined();
expect(result.current.error).toBeNull();
});
it('refetches when URL changes', async () => {
const { result, rerender } = renderHook(
({ url }) => useFetch(url),
{ initialProps: { url: '/api/users' } }
);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
const firstData = result.current.data;
rerender({ url: '/api/posts' });
await waitFor(() => {
expect(result.current.data).not.toEqual(firstData);
});
});
});import { renderHook } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { useTheme } from './useTheme';
describe('useTheme Hook', () => {
const wrapper = ({ children }) => (
<ThemeProvider initialTheme="light">
{children}
</ThemeProvider>
);
it('returns current theme', () => {
const { result } = renderHook(() => useTheme(), { wrapper });
expect(result.current.theme).toBe('light');
});
it('toggles theme', () => {
const { result } = renderHook(() => useTheme(), { wrapper });
act(() => {
result.current.toggleTheme();
});
expect(result.current.theme).toBe('dark');
});
});import { renderHook, waitFor } from '@testing-library/react';
import { useAsyncData } from './useAsyncData';
describe('useAsyncData Hook', () => {
it('loads data asynchronously', async () => {
const { result } = renderHook(() => useAsyncData('/api/data'));
expect(result.current.loading).toBe(true);
expect(result.current.data).toBeNull();
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.data).toBeDefined();
});
it('handles errors', async () => {
const { result } = renderHook(() => useAsyncData('/api/error'));
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.error).toBeDefined();
expect(result.current.data).toBeNull();
});
});import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from './App';
describe('App Integration', () => {
it('navigates between pages', async () => {
const user = userEvent.setup();
render(<App />);
expect(screen.getByText(/home page/i)).toBeInTheDocument();
const aboutLink = screen.getByRole('link', { name: /about/i });
await user.click(aboutLink);
expect(screen.getByText(/about page/i)).toBeInTheDocument();
});
it('completes full user flow', async () => {
const user = userEvent.setup();
render(<App />);
// Navigate to signup
await user.click(screen.getByRole('link', { name: /sign up/i }));
// Fill out form
await user.type(screen.getByLabelText(/email/i), 'user@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
// Submit form
await user.click(screen.getByRole('button', { name: /submit/i }));
// Verify success
expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
});
});import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { AppRoutes } from './AppRoutes';
const renderWithRouter = (ui, { initialEntries = ['/'] } = {}) => {
return render(
<MemoryRouter initialEntries={initialEntries}>
{ui}
</MemoryRouter>
);
};
describe('AppRoutes Integration', () => {
it('renders home page by default', () => {
renderWithRouter(<AppRoutes />);
expect(screen.getByText(/home/i)).toBeInTheDocument();
});
it('renders user page at /users/:id', () => {
renderWithRouter(<AppRoutes />, { initialEntries: ['/users/123'] });
expect(screen.getByText(/user profile/i)).toBeInTheDocument();
});
it('navigates programmatically', async () => {
const user = userEvent.setup();
renderWithRouter(<AppRoutes />);
const navButton = screen.getByRole('button', { name: /go to profile/i });
await user.click(navButton);
expect(screen.getByText(/profile page/i)).toBeInTheDocument();
});
});import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import userEvent from '@testing-library/user-event';
import { TodoList } from './TodoList';
import todosReducer from './todosSlice';
const createMockStore = (initialState = {}) => {
return configureStore({
reducer: {
todos: todosReducer,
},
preloadedState: initialState,
});
};
const renderWithStore = (ui, { store = createMockStore() } = {}) => {
return render(<Provider store={store}>{ui}</Provider>);
};
describe('TodoList Integration', () => {
it('adds new todo', async () => {
const user = userEvent.setup();
renderWithStore(<TodoList />);
const input = screen.getByPlaceholderText(/new todo/i);
const addButton = screen.getByRole('button', { name: /add/i });
await user.type(input, 'Buy groceries');
await user.click(addButton);
expect(screen.getByText(/buy groceries/i)).toBeInTheDocument();
});
it('renders initial todos from store', () => {
const initialState = {
todos: {
items: [
{ id: 1, text: 'Existing todo', completed: false },
],
},
};
renderWithStore(<TodoList />, { store: createMockStore(initialState) });
expect(screen.getByText(/existing todo/i)).toBeInTheDocument();
});
});// Element presence
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();
// Visibility
expect(element).toBeVisible();
expect(element).not.toBeVisible();
// Text content
expect(element).toHaveTextContent('Hello World');
expect(element).toHaveTextContent(/hello/i);
// Attributes
expect(element).toHaveAttribute('type', 'submit');
expect(element).toHaveAttribute('disabled');
// Classes
expect(element).toHaveClass('active');
expect(element).toHaveClass('btn', 'btn-primary');
// Styles
expect(element).toHaveStyle({ color: 'red' });
expect(element).toHaveStyle('display: none');
// Forms
expect(input).toHaveValue('test');
expect(input).toHaveDisplayValue('Test');
expect(checkbox).toBeChecked();
expect(checkbox).not.toBeChecked();
expect(input).toBeDisabled();
expect(input).toBeEnabled();
expect(input).toBeRequired();
expect(input).toBeInvalid();
expect(input).toBeValid();
// Focus
expect(element).toHaveFocus();
// Accessibility
expect(element).toHaveAccessibleName('Submit button');
expect(element).toHaveAccessibleDescription('Click to submit form');
// Contains
expect(container).toContainElement(child);
expect(container).toContainHTML('<span>Text</span>');describedescribe('UserProfile', () => {
describe('when loading', () => {
it('shows loading spinner', () => {});
});
describe('when loaded', () => {
it('displays user information', () => {});
it('shows profile picture', () => {});
});
});// Good
it('displays error message when login fails', () => {});
// Bad
it('test login', () => {});it('increments counter', async () => {
// Arrange
const user = userEvent.setup();
render(<Counter />);
// Act
await user.click(screen.getByRole('button', { name: /increment/i }));
// Assert
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});it('renders list of items', () => {
const items = ['Apple', 'Banana', 'Cherry'];
render(<ItemList items={items} />);
items.forEach(item => {
expect(screen.getByText(item)).toBeInTheDocument();
});
});it('has accessible form', () => {
render(<ContactForm />);
const nameInput = screen.getByLabelText(/name/i);
expect(nameInput).toHaveAccessibleName('Name');
expect(nameInput).toBeRequired();
const submitButton = screen.getByRole('button', { name: /submit/i });
expect(submitButton).toHaveAttribute('type', 'submit');
});it('catches errors and displays fallback', () => {
const ThrowError = () => {
throw new Error('Test error');
};
// Suppress console.error for this test
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>Something went wrong</div>}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
spy.mockRestore();
});it('renders modal in portal', () => {
render(<Modal isOpen={true}>Modal Content</Modal>);
const modal = screen.getByText(/modal content/i);
expect(modal).toBeInTheDocument();
// Modal should be in document.body, not in the component tree
expect(modal.parentElement).toBe(document.body);
});