Loading...
Loading...
Compare original and translation side by side
/** @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;/** @type {import('jest').Config} */
const config = {
// DOM测试的测试环境
testEnvironment: 'jsdom',
// 环境初始化后的设置文件
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
// 模块路径
moduleDirectories: ['node_modules', 'src'],
// 使用babel-jest转换文件
transform: {
'^.+\\.(js|jsx)$': 'babel-jest',
},
// 静态资源和CSS的模块名称映射
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
},
// 覆盖率配置
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/index.js',
'!src/**/*.test.{js,jsx}',
'!src/**/__tests__/**',
],
// 覆盖率阈值
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();
});// 从jest-dom添加自定义jest匹配器
import '@testing-library/jest-dom';
// (可选)使用jest-extended匹配器扩展expect
import * as matchers from 'jest-extended';
expect.extend(matchers);
// 模拟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(),
})),
});
// 模拟IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
};
// (可选)在测试中抑制控制台错误
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;
});
// 每次测试后重置模拟
afterEach(() => {
jest.clearAllMocks();
});module.exports = 'test-file-stub';module.exports = {};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')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 version// 单个元素查询
screen.getByRole('button') // 未找到或找到多个时抛出错误
screen.queryByRole('button') // 未找到时返回null
await screen.findByRole('button') // 异步方法,最多等待1000ms
// 多个元素查询
screen.getAllByRole('listitem') // 未找到任何元素时抛出错误
screen.queryAllByRole('listitem') // 未找到时返回[]
await screen.findAllByRole('listitem') // 异步版本import { 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 { Greeting } from './Greeting';
describe('Greeting Component', () => {
it('渲染问候消息', () => {
render(<Greeting name="Alice" />);
expect(screen.getByText(/hello, alice/i)).toBeInTheDocument();
});
it('未提供名称时渲染默认问候语', () => {
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 { Counter } from './Counter';
describe('Counter Component', () => {
it('点击按钮时增加计数器值', 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('点击按钮时减少计数器值', 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 userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm Component', () => {
it('提交包含用户名和密码的表单', 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('为空字段显示验证错误', 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();
});
});import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';
describe('UserProfile Component', () => {
it('加载时显示加载状态', () => {
render(<UserProfile loading={true} />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
});
it('加载完成后显示用户数据', () => {
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('发生错误时显示错误消息', () => {
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');
});
});// __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('成功获取用户', 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('处理获取数据时的错误', 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',
})
);
});
});import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';
describe('Button Component', () => {
it('点击时调用onClick处理函数', 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('调用onClick时传入事件对象', 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();
});
});
});// 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('获取并显示用户', async () => {
render(<UserList />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/alice/i)).toBeInTheDocument();
expect(screen.getByText(/bob/i)).toBeInTheDocument();
});
});
it('处理服务器错误', 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 { 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('为已认证用户渲染内容', () => {
const contextValue = mockAuthContext();
render(
<AuthContext.Provider value={contextValue}>
<ProtectedComponent />
</AuthContext.Provider>
);
expect(screen.getByText(/welcome, test user/i)).toBeInTheDocument();
});
it('为未认证用户渲染登录提示', () => {
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 } from '@testing-library/react';
import { ParentComponent } from './ParentComponent';
// 模拟子组件
jest.mock('./ChildComponent', () => ({
ChildComponent: ({ title, onAction }) => (
<div>
<h2>{title}</h2>
<button onClick={onAction}>Mocked Action</button>
</div>
),
}));
describe('ParentComponent', () => {
it('使用模拟的子组件渲染', () => {
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, waitFor } from '@testing-library/react';
import { AsyncComponent } from './AsyncComponent';
describe('AsyncComponent', () => {
it('加载并显示数据', async () => {
render(<AsyncComponent />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/data loaded/i)).toBeInTheDocument();
});
});
it('等待特定条件满足', 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 { DataFetcher } from './DataFetcher';
describe('DataFetcher Component', () => {
it('显示获取到的数据', async () => {
render(<DataFetcher />);
// findBy会自动等待元素出现
const heading = await screen.findByRole('heading', { name: /data/i });
expect(heading).toBeInTheDocument();
});
it('等待不存在的元素时超时', 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 } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AsyncForm } from './AsyncForm';
describe('AsyncForm Component', () => {
it('提交表单并显示成功消息', 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('提交失败时显示错误消息', 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 { render, screen, act } from '@testing-library/react';
import { Timer } from './Timer';
describe('Timer Component', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('每秒更新定时器', () => {
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('卸载时清理定时器', () => {
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 { useCounter } from './useCounter';
describe('useCounter Hook', () => {
it('使用默认值初始化', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('使用提供的值初始化', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('增加计数器值', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('减少计数器值', () => {
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 { useFetch } from './useFetch';
describe('useFetch Hook', () => {
it('为给定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('URL变化时重新获取数据', 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 } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { useTheme } from './useTheme';
describe('useTheme Hook', () => {
const wrapper = ({ children }) => (
<ThemeProvider initialTheme="light">
{children}
</ThemeProvider>
);
it('返回当前主题', () => {
const { result } = renderHook(() => useTheme(), { wrapper });
expect(result.current.theme).toBe('light');
});
it('切换主题', () => {
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 { renderHook, waitFor } from '@testing-library/react';
import { useAsyncData } from './useAsyncData';
describe('useAsyncData Hook', () => {
it('异步加载数据', 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('处理错误', 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 userEvent from '@testing-library/user-event';
import { App } from './App';
describe('App Integration', () => {
it('在页面之间导航', 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('完成完整用户流程', async () => {
const user = userEvent.setup();
render(<App />);
// 导航到注册页面
await user.click(screen.getByRole('link', { name: /sign up/i }));
// 填写表单
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(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 { 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('默认渲染首页', () => {
renderWithRouter(<AppRoutes />);
expect(screen.getByText(/home/i)).toBeInTheDocument();
});
it('在/users/:id路径渲染用户页面', () => {
renderWithRouter(<AppRoutes />, { initialEntries: ['/users/123'] });
expect(screen.getByText(/user profile/i)).toBeInTheDocument();
});
it('编程式导航', 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();
});
});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('添加新待办事项', 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('渲染来自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>');// 元素存在性
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();
// 可见性
expect(element).toBeVisible();
expect(element).not.toBeVisible();
// 文本内容
expect(element).toHaveTextContent('Hello World');
expect(element).toHaveTextContent(/hello/i);
// 属性
expect(element).toHaveAttribute('type', 'submit');
expect(element).toHaveAttribute('disabled');
// 类名
expect(element).toHaveClass('active');
expect(element).toHaveClass('btn', 'btn-primary');
// 样式
expect(element).toHaveStyle({ color: 'red' });
expect(element).toHaveStyle('display: none');
// 表单
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();
// 焦点
expect(element).toHaveFocus();
// 可访问性
expect(element).toHaveAccessibleName('Submit button');
expect(element).toHaveAccessibleDescription('Click to submit form');
// 包含关系
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();
});describedescribe('UserProfile', () => {
describe('加载时', () => {
it('显示加载动画', () => {});
});
describe('加载完成后', () => {
it('显示用户信息', () => {});
it('显示头像', () => {});
});
});// 好的命名
it('登录失败时显示错误消息', () => {});
// 不好的命名
it('测试登录', () => {});it('增加计数器值', async () => {
// 准备
const user = userEvent.setup();
render(<Counter />);
// 执行
await user.click(screen.getByRole('button', { name: /increment/i }));
// 断言
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('渲染项目列表', () => {
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('表单符合可访问性标准', () => {
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('捕获错误并显示回退内容', () => {
const ThrowError = () => {
throw new Error('Test error');
};
// 在此测试中抑制console.error
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
render(
<ErrorBoundary fallback={<div>出现错误</div>}>
<ThrowError />
</ErrorBoundary>
);
expect(screen.getByText(/出现错误/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);
});it('在Portal中渲染模态框', () => {
render(<Modal isOpen={true}>Modal Content</Modal>);
const modal = screen.getByText(/modal content/i);
expect(modal).toBeInTheDocument();
// 模态框应在document.body中,而非组件树内
expect(modal.parentElement).toBe(document.body);
});