This skill provides the complete SOP and best practices for unit testing TDesign React components.
Trigger Conditions
Use this skill when the user needs to perform the following tasks:
- Write unit tests for components
- Update or fix test cases
- Run snapshot tests
- Improve test coverage
Testing Frameworks and Tools
| Tool | Purpose |
|---|---|
| Vitest | Test runner |
| @testing-library/react | React component testing tool |
| @testing-library/user-event | User interaction simulation |
| @testing-library/jest-dom | DOM assertion extensions |
Test File Specifications
File Location and Naming
packages/components/[component-name]/
└── __tests__/
├── vitest-[component-name].test.jsx # 单元测试
└── __snapshots__/ # 快照文件(自动生成)
Naming Format:vitest-[component-name].test.jsx
⚠️ Note: Test files are generated by the API platform, and the file header has a comment marker.
Test Tool Imports
import { render, fireEvent, vi } from '@test/utils';
import { ComponentName } from '..';
@test/utils has encapsulated commonly used tools, including:
render- Component renderingfireEvent- Event triggeringvi- Vitest mock toolsact- React act wrapperuserEvent- User interaction simulationmockDelay- Asynchronous delay simulationsimulateInputChange- Input simulationsimulateKeydownEvent- Keyboard event simulation
SOP Process
Step 1: Basic Rendering Test
Test whether the component renders normally:
describe('ExampleComponent', () => {
it('renders correctly', () => {
const { container } = render(<ExampleComponent />);
expect(container.firstChild).toHaveClass('t-example-component');
});
});
Step 2: Props Test
Write test cases for each 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();
});
});
Step 3: TNode Rendering Test
Test custom rendered content:
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();
});
Step 4: Event Test
Test component event callbacks:
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();
});
Step 5: Asynchronous Operation Test
Test asynchronous behavior:
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();
});
Step 6: User Interaction Test
Use userEvent for complex interaction tests:
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');
});
Snapshot Testing
CSR Snapshot Testing
Location:test/snap/csr.test.jsx
Automatically generate snapshots for all _example/*.tsx :
# 运行 CSR 快照测试
NODE_ENV=test-snap pnpm test
SSR Snapshot Testing
Location:test/snap/ssr.test.jsx
Test server-side rendering output.
Update Snapshots
# 更新所有快照
pnpm test -- --update
# 更新特定组件快照
pnpm test button -- --update
Run test commands
# 运行所有单元测试
pnpm test
# 运行特定组件测试
pnpm test button
# 运行测试并生成覆盖率报告
pnpm test -- --coverage
# 运行快照测试
NODE_ENV=test-snap pnpm test
# 交互式测试模式
pnpm test -- --ui
Common Test Patterns
Test Loading State
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();
});
Test DOM Attributes
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');
});
Test className Combination
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');
});
Mock ResizeObserver
class ResizeObserver {
observe() { return this; }
unobserve() { return this; }
}
beforeAll(() => {
global.ResizeObserver = ResizeObserver;
});
Mock Time
import MockDate from 'mockdate';
beforeEach(() => {
MockDate.set('2020-12-28 00:00:00');
});
afterEach(() => {
MockDate.reset();
});
Test Checklist
When writing tests, ensure coverage of:
- Default rendering
- All boolean props (both true/false cases)
- Every value of all enum props
- TNode type props
- Event callback triggering
- Behavior in disabled state
- Loading state
- Edge cases (empty values, extreme values)
- Snapshot testing
Test Coverage Requirements
- Overall coverage target: > 80%
- New component requirement: > 90% line coverage
- Core logic: 100% branch coverage
Common Questions
Q: Can test files be edited manually?
A: vitest-*.test.jsx Files are generated by the API platform, and the file header has a comment marker. Additional tests can be added on top of them, but core tests should be maintained through the API platform.
Q: How to handle asynchronous components?
A: Use mockDelay or act wrapper, and ensure the asynchronous operation is complete before making assertions.
Q: What should I do if a snapshot test fails?
A: First check whether the change is expected. If it is an expected change, use the --update parameter to update the snapshot.