testing-library by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill testing-library状态 : 生产就绪 最后更新 : 2026-02-06 版本 : 16.x 用户事件 : 14.x
# 使用 Vitest 安装
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
# 或使用 Jest
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// 每个测试后清理
afterEach(() => {
cleanup();
});
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
},
});
按照此顺序使用查询,以实现无障碍且健壮的测试:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 优先级 | 查询 | 用于 |
|---|
| 1 | getByRole | 按钮、链接、标题、输入框 |
| 2 | getByLabelText | 带有标签的表单输入框 |
| 3 | getByPlaceholderText | 没有可见标签的输入框 |
| 4 | getByText | 非交互式文本内容 |
| 5 | getByTestId | 仅作为最后的手段 |
import { render, screen } from '@testing-library/react';
// ✅ 良好 - 语义化角色查询
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('link', { name: /learn more/i });
// ✅ 良好 - 基于标签的表单查询
screen.getByLabelText(/email address/i);
// ⚠️ 尚可 - 当没有更好的选择时
screen.getByText(/welcome to our app/i);
// ❌ 避免 - 非无障碍,脆弱
screen.getByTestId('submit-button');
| 变体 | 返回 | 抛出异常 | 用于 |
|---|---|---|---|
getBy | 元素 | 是 | 元素现在存在 |
queryBy | 元素或 null | 否 | 元素可能不存在 |
findBy | Promise | 是 | 异步,稍后出现 |
getAllBy | 元素[] | 是 | 多个元素 |
queryAllBy | 元素[] | 否 | 多个或没有 |
findAllBy | Promise<元素[]> | 是 | 多个,异步 |
// 元素立即存在
const button = screen.getByRole('button');
// 检查元素不存在
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// 等待异步元素出现
const modal = await screen.findByRole('dialog');
// 多个元素
const items = screen.getAllByRole('listitem');
始终使用 userEvent 而非 fireEvent - 它模拟真实用户行为。
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('表单', () => {
it('提交表单数据', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
// 在输入框中输入
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'secret123');
// 点击提交
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'secret123',
});
});
});
const user = userEvent.setup();
// 点击
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element); // 全选文本
// 输入
await user.type(input, 'hello world');
await user.clear(input);
await user.type(input, '{Enter}'); // 特殊键
// 键盘
await user.keyboard('{Shift>}A{/Shift}'); // Shift+A
await user.tab(); // Tab 导航
// 选择
await user.selectOptions(select, ['option1', 'option2']);
// 悬停
await user.hover(element);
await user.unhover(element);
// 剪贴板
await user.copy();
await user.paste();
it('先显示加载状态,然后显示内容', async () => {
render(<AsyncComponent />);
// 初始显示加载中
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// 等待内容出现(自动重试)
const content = await screen.findByText(/data loaded/i);
expect(content).toBeInTheDocument();
});
import { waitFor } from '@testing-library/react';
it('点击后更新计数', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /increment/i }));
// 等待状态更新
await waitFor(() => {
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
});
import { waitForElementToBeRemoved } from '@testing-library/react';
it('关闭后隐藏模态框', async () => {
const user = userEvent.setup();
render(<ModalComponent />);
await user.click(screen.getByRole('button', { name: /close/i }));
// 等待模态框消失
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});
使用 Mock Service Worker 在网络层面模拟 API 调用。
pnpm add -D msw
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({
id: 1,
name: 'Test User',
email: 'test@example.com',
});
}),
http.post('/api/login', async ({ request }) => {
const body = await request.json();
if (body.password === 'correct') {
return HttpResponse.json({ token: 'abc123' });
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
];
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/test/setup.ts
import { server } from './mocks/server';
import { beforeAll, afterEach, afterAll } from 'vitest';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
import { server } from '../test/mocks/server';
import { http, HttpResponse } from 'msw';
it('处理 API 错误', async () => {
// 为此测试覆盖处理器
server.use(
http.get('/api/user', () => {
return HttpResponse.json(
{ error: 'Server error' },
{ status: 500 }
);
})
);
render(<UserProfile />);
await screen.findByText(/error loading user/i);
});
pnpm add -D @axe-core/react
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('没有无障碍访问违规', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
使用 getByRole 隐式测试无障碍访问:
// 仅当按钮正确实现无障碍访问时,此测试才会通过
screen.getByRole('button', { name: /submit/i });
// 在以下情况下会失败:
// - 元素不是按钮或没有 role="button"
// - 无障碍名称不匹配
// - 元素对无障碍树隐藏
it('验证必填字段', async () => {
const user = userEvent.setup();
render(<ContactForm />);
// 不填写必填字段就提交
await user.click(screen.getByRole('button', { name: /submit/i }));
// 检查验证错误
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/message is required/i)).toBeInTheDocument();
});
it('打开和关闭模态框', async () => {
const user = userEvent.setup();
render(<ModalTrigger />);
// 初始模态框不可见
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// 打开模态框
await user.click(screen.getByRole('button', { name: /open/i }));
expect(screen.getByRole('dialog')).toBeInTheDocument();
// 关闭模态框
await user.click(screen.getByRole('button', { name: /close/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});
it('渲染列表项', () => {
render(<TodoList items={['Buy milk', 'Walk dog']} />);
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(2);
expect(items[0]).toHaveTextContent('Buy milk');
});
// 存在性
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEmptyDOMElement();
// 状态
expect(button).toBeEnabled();
expect(button).toBeDisabled();
expect(checkbox).toBeChecked();
expect(input).toBeRequired();
// 内容
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveValue('test');
expect(element).toHaveAttribute('href', '/about');
// 样式
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });
// 焦点
expect(input).toHaveFocus();
it('调试渲染', () => {
render(<MyComponent />);
// 打印整个 DOM
screen.debug();
// 打印特定元素
screen.debug(screen.getByRole('button'));
});
import { logRoles } from '@testing-library/react';
it('显示可用角色', () => {
const { container } = render(<MyComponent />);
logRoles(container);
});
// ❌ 错误 - 如果元素异步出现会失败
const modal = screen.getByRole('dialog');
// ✅ 正确 - 等待元素
const modal = await screen.findByRole('dialog');
// ❌ 错误 - 竞态条件
user.click(button);
expect(result).toBeInTheDocument();
// ✅ 正确 - 等待交互完成
await user.click(button);
expect(result).toBeInTheDocument();
// ❌ 错误 - 非无障碍,脆弱
const button = container.querySelector('.submit-btn');
// ✅ 正确 - 无障碍查询
const button = screen.getByRole('button', { name: /submit/i });
vitest 技能 - 测试运行器配置testing-patterns 技能 - 通用测试模式每周安装数
202
仓库
GitHub 星标数
643
首次出现
2026年2月6日
安全审计
安装于
cursor133
claude-code130
opencode112
gemini-cli106
replit103
codex99
Status : Production Ready Last Updated : 2026-02-06 Version : 16.x User Event : 14.x
# Install with Vitest
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
# Or with Jest
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// vitest.config.ts
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
},
});
Use queries in this order for accessible, resilient tests:
| Priority | Query | Use For |
|---|---|---|
| 1 | getByRole | Buttons, links, headings, inputs |
| 2 | getByLabelText | Form inputs with labels |
| 3 | getByPlaceholderText | Inputs without visible labels |
| 4 | getByText | Non-interactive text content |
| 5 | getByTestId | Last resort only |
import { render, screen } from '@testing-library/react';
// ✅ GOOD - semantic role queries
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('link', { name: /learn more/i });
// ✅ GOOD - label-based queries for forms
screen.getByLabelText(/email address/i);
// ⚠️ OK - when no better option
screen.getByText(/welcome to our app/i);
// ❌ AVOID - not accessible, brittle
screen.getByTestId('submit-button');
| Variant | Returns | Throws | Use For |
|---|---|---|---|
getBy | Element | Yes | Element exists now |
queryBy | Element or null | No | Element might not exist |
findBy | Promise | Yes | Async, appears later |
getAllBy | Element[] | Yes | Multiple elements |
// Element exists immediately
const button = screen.getByRole('button');
// Check element doesn't exist
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// Wait for async element to appear
const modal = await screen.findByRole('dialog');
// Multiple elements
const items = screen.getAllByRole('listitem');
Always useuserEvent over fireEvent - it simulates real user behavior.
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('Form', () => {
it('submits form data', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
// Type in inputs
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'secret123');
// Click submit
await user.click(screen.getByRole('button', { name: /sign in/i }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'secret123',
});
});
});
const user = userEvent.setup();
// Clicking
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element); // Select all text
// Typing
await user.type(input, 'hello world');
await user.clear(input);
await user.type(input, '{Enter}'); // Special keys
// Keyboard
await user.keyboard('{Shift>}A{/Shift}'); // Shift+A
await user.tab(); // Tab navigation
// Selection
await user.selectOptions(select, ['option1', 'option2']);
// Hover
await user.hover(element);
await user.unhover(element);
// Clipboard
await user.copy();
await user.paste();
it('shows loading then content', async () => {
render(<AsyncComponent />);
// Shows loading initially
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// Wait for content to appear (auto-retries)
const content = await screen.findByText(/data loaded/i);
expect(content).toBeInTheDocument();
});
import { waitFor } from '@testing-library/react';
it('updates count after click', async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button', { name: /increment/i }));
// Wait for state update
await waitFor(() => {
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});
});
import { waitForElementToBeRemoved } from '@testing-library/react';
it('hides modal after close', async () => {
const user = userEvent.setup();
render(<ModalComponent />);
await user.click(screen.getByRole('button', { name: /close/i }));
// Wait for modal to disappear
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});
Mock API calls at the network level with Mock Service Worker.
pnpm add -D msw
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/user', () => {
return HttpResponse.json({
id: 1,
name: 'Test User',
email: 'test@example.com',
});
}),
http.post('/api/login', async ({ request }) => {
const body = await request.json();
if (body.password === 'correct') {
return HttpResponse.json({ token: 'abc123' });
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
];
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/test/setup.ts
import { server } from './mocks/server';
import { beforeAll, afterEach, afterAll } from 'vitest';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
import { server } from '../test/mocks/server';
import { http, HttpResponse } from 'msw';
it('handles API error', async () => {
// Override handler for this test
server.use(
http.get('/api/user', () => {
return HttpResponse.json(
{ error: 'Server error' },
{ status: 500 }
);
})
);
render(<UserProfile />);
await screen.findByText(/error loading user/i);
});
pnpm add -D @axe-core/react
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
it('has no accessibility violations', async () => {
const { container } = render(<MyComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Using getByRole implicitly tests accessibility:
// This passes only if button is properly accessible
screen.getByRole('button', { name: /submit/i });
// Fails if:
// - Element isn't a button or role="button"
// - Accessible name doesn't match
// - Element is hidden from accessibility tree
it('validates required fields', async () => {
const user = userEvent.setup();
render(<ContactForm />);
// Submit without filling required fields
await user.click(screen.getByRole('button', { name: /submit/i }));
// Check for validation errors
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/message is required/i)).toBeInTheDocument();
});
it('opens and closes modal', async () => {
const user = userEvent.setup();
render(<ModalTrigger />);
// Modal not visible initially
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// Open modal
await user.click(screen.getByRole('button', { name: /open/i }));
expect(screen.getByRole('dialog')).toBeInTheDocument();
// Close modal
await user.click(screen.getByRole('button', { name: /close/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});
it('renders list items', () => {
render(<TodoList items={['Buy milk', 'Walk dog']} />);
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(2);
expect(items[0]).toHaveTextContent('Buy milk');
});
// Presence
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEmptyDOMElement();
// State
expect(button).toBeEnabled();
expect(button).toBeDisabled();
expect(checkbox).toBeChecked();
expect(input).toBeRequired();
// Content
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveValue('test');
expect(element).toHaveAttribute('href', '/about');
// Styles
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });
// Focus
expect(input).toHaveFocus();
it('debugs rendering', () => {
render(<MyComponent />);
// Print entire DOM
screen.debug();
// Print specific element
screen.debug(screen.getByRole('button'));
});
import { logRoles } from '@testing-library/react';
it('shows available roles', () => {
const { container } = render(<MyComponent />);
logRoles(container);
});
// ❌ WRONG - fails if element appears async
const modal = screen.getByRole('dialog');
// ✅ CORRECT - waits for element
const modal = await screen.findByRole('dialog');
// ❌ WRONG - race condition
user.click(button);
expect(result).toBeInTheDocument();
// ✅ CORRECT - await the interaction
await user.click(button);
expect(result).toBeInTheDocument();
// ❌ WRONG - not accessible, brittle
const button = container.querySelector('.submit-btn');
// ✅ CORRECT - accessible query
const button = screen.getByRole('button', { name: /submit/i });
vitest skill - Test runner configurationtesting-patterns skill - General testing patternsWeekly Installs
202
Repository
GitHub Stars
643
First Seen
Feb 6, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
cursor133
claude-code130
opencode112
gemini-cli106
replit103
codex99
Vue 3 调试指南:解决响应式、计算属性与监听器常见错误
11,900 周安装
queryAllBy| Element[] |
| No |
| Multiple or none |
findAllBy | Promise<Element[]> | Yes | Multiple, async |