SuperAPI AI Directory

tdesign-test-component

Testing & quality

View repository

Updated 10-16 10:40

About this skill

TDesign Component Testing Guide

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 rendering
  • fireEvent - Event triggering
  • vi - Vitest mock tools
  • act - React act wrapper
  • userEvent - User interaction simulation
  • mockDelay - Asynchronous delay simulation
  • simulateInputChange - Input simulation
  • simulateKeydownEvent - 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.