بنقرة واحدة
tdesign-test-component
// This skill should be used when writing or maintaining unit tests for TDesign React components. It provides testing patterns, best practices, and SOP for Vitest-based component testing including snapshot tests.
// This skill should be used when writing or maintaining unit tests for TDesign React components. It provides testing patterns, best practices, and SOP for Vitest-based component testing including snapshot tests.
This skill should be used when developing or modifying styles for TDesign React components. It provides guidance on BEM naming conventions, CSS Variables usage, state classes, and the relationship between component code and common submodule styles.
This skill should be used when creating a new TDesign React component. It provides a complete SOP for scaffolding components in the packages/components or packages/pro-components directories, including file structure, code patterns, and registration steps.
This skill should be used when developing or modifying TDesign Pro Components (AIGC/Chat). It provides the ChatEngine architecture, component patterns, AG-UI protocol integration, and Generative UI development guide.
| name | tdesign-test-component |
| description | This skill should be used when writing or maintaining unit tests for TDesign React components. It provides testing patterns, best practices, and SOP for Vitest-based component testing including snapshot tests. |
本技能提供 TDesign React 组件单元测试的完整 SOP 和最佳实践。
当用户需要执行以下任务时使用此技能:
| 工具 | 用途 |
|---|---|
| Vitest | 测试运行器 |
| @testing-library/react | React 组件测试工具 |
| @testing-library/user-event | 用户交互模拟 |
| @testing-library/jest-dom | DOM 断言扩展 |
packages/components/[component-name]/
└── __tests__/
├── vitest-[component-name].test.jsx # 单元测试
└── __snapshots__/ # 快照文件(自动生成)
命名格式:vitest-[component-name].test.jsx
⚠️ 注意:测试文件由 API 平台生成,文件头部有注释标记。
import { render, fireEvent, vi } from '@test/utils';
import { ComponentName } from '..';
@test/utils 已封装常用工具,包括:
render - 组件渲染fireEvent - 事件触发vi - Vitest mock 工具act - React act wrapperuserEvent - 用户交互模拟mockDelay - 异步延迟模拟simulateInputChange - 输入模拟simulateKeydownEvent - 键盘事件模拟测试组件能否正常渲染:
describe('ExampleComponent', () => {
it('renders correctly', () => {
const { container } = render(<ExampleComponent />);
expect(container.firstChild).toHaveClass('t-example-component');
});
});
为每个 prop 编写测试用例:
// Boolean prop 测试模式
it('props.disabled works fine', () => {
// 默认值测试
const { container: container1 } = render(<Button>Text</Button>);
expect(container1.querySelector('.t-is-disabled')).toBeFalsy();
// disabled = true
const { container: container2 } = render(<Button disabled={true}>Text</Button>);
expect(container2.firstChild).toHaveClass('t-is-disabled');
expect(container2).toMatchSnapshot();
// disabled = false
const { container: container3 } = render(<Button disabled={false}>Text</Button>);
expect(container3.querySelector('.t-is-disabled')).toBeFalsy();
});
// 枚举 prop 测试模式
['small', 'medium', 'large'].forEach((item) => {
it(`props.size is equal to ${item}`, () => {
const { container } = render(<Button size={item}>Text</Button>);
// medium 是默认值,不添加额外类名
if (item !== 'medium') {
expect(container.firstChild).toHaveClass(`t-size-${item[0]}`);
}
expect(container).toMatchSnapshot();
});
});
测试自定义渲染内容:
it('props.children works fine', () => {
const { container } = render(
<Button>
<span className="custom-node">TNode</span>
</Button>
);
expect(container.querySelector('.custom-node')).toBeTruthy();
expect(container).toMatchSnapshot();
});
it('props.icon works fine', () => {
const { container } = render(
<Button icon={<span className="custom-icon">Icon</span>}>Text</Button>
);
expect(container.querySelector('.custom-icon')).toBeTruthy();
});
测试组件事件回调:
it('events.click works fine', () => {
const fn = vi.fn();
const { container } = render(<Button onClick={fn}>Click</Button>);
fireEvent.click(container.firstChild);
expect(fn).toHaveBeenCalled();
expect(fn.mock.calls[0][0].type).toBe('click');
});
// 禁用状态下事件不触发
it('disabled button should not trigger click', () => {
const fn = vi.fn();
const { container } = render(<Button disabled onClick={fn}>Click</Button>);
fireEvent.click(container.firstChild);
expect(fn).not.toHaveBeenCalled();
});
测试异步行为:
import { mockDelay } from '@test/utils';
it('async operation works fine', async () => {
const { container } = render(<AsyncComponent />);
// 触发异步操作
fireEvent.click(container.querySelector('.trigger'));
// 等待异步完成
await mockDelay(300);
expect(container.querySelector('.result')).toBeTruthy();
});
使用 userEvent 进行复杂交互测试:
import { userEvent } from '@test/utils';
it('user input works fine', async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const { container } = render(<Input onChange={onChange} />);
const input = container.querySelector('input');
await user.type(input, 'hello');
expect(onChange).toHaveBeenCalled();
expect(input.value).toBe('hello');
});
位置:test/snap/csr.test.jsx
自动为所有 _example/*.tsx 生成快照:
# 运行 CSR 快照测试
NODE_ENV=test-snap pnpm test
位置:test/snap/ssr.test.jsx
测试服务端渲染输出。
# 更新所有快照
pnpm test -- --update
# 更新特定组件快照
pnpm test button -- --update
# 运行所有单元测试
pnpm test
# 运行特定组件测试
pnpm test button
# 运行测试并生成覆盖率报告
pnpm test -- --coverage
# 运行快照测试
NODE_ENV=test-snap pnpm test
# 交互式测试模式
pnpm test -- --ui
it('loading state shows loading indicator', () => {
const { container } = render(<Button loading>Text</Button>);
expect(container.firstChild).toHaveClass('t-is-loading');
expect(container.querySelector('.t-loading')).toBeTruthy();
});
it('href attribute works fine', () => {
const { container } = render(<Button href="https://example.com">Link</Button>);
expect(container.firstChild.getAttribute('href')).toBe('https://example.com');
expect(container.firstChild.tagName.toLowerCase()).toBe('a');
});
it('custom className is merged', () => {
const { container } = render(<Button className="custom-class">Text</Button>);
expect(container.firstChild).toHaveClass('t-button');
expect(container.firstChild).toHaveClass('custom-class');
});
class ResizeObserver {
observe() { return this; }
unobserve() { return this; }
}
beforeAll(() => {
global.ResizeObserver = ResizeObserver;
});
import MockDate from 'mockdate';
beforeEach(() => {
MockDate.set('2020-12-28 00:00:00');
});
afterEach(() => {
MockDate.reset();
});
编写测试时,确保覆盖:
A: vitest-*.test.jsx 文件由 API 平台生成,文件头有注释标记。可以在其基础上添加额外测试,但核心测试应通过 API 平台维护。
A: 使用 mockDelay 或 act wrapper,确保异步操作完成后再进行断言。
A: 首先检查变更是否预期内。如果是预期变更,使用 --update 参数更新快照。