---
title: 测试文档
order: 4
category: pro
---

# PisellGridPro 测试文档

## 测试框架

- **测试框架**：Jest
- **测试工具**：@testing-library/react
- **快照测试**：Jest Snapshot
- **覆盖率要求**：80% 以上

## 测试策略

### 1. 单元测试

针对单个组件进行独立测试。

### 2. 集成测试

测试多个组件协同工作的场景。

### 3. 快照测试

确保组件渲染结果的稳定性。

### 4. 样式测试

测试组件的样式类名和内联样式。

## 核心组件测试

### 1. PisellGridPro 主容器测试

```
import { render, screen } from '@testing-library/react';
import PisellGridPro from '../index';

describe('PisellGridPro', () => {
  it('应该正确渲染', () => {
    const { container } = render(
      <PisellGridPro>
        <div>测试内容</div>
      </PisellGridPro>
    );

    expect(container.querySelector('.pisell-grid-pro')).toBeInTheDocument();
    expect(screen.getByText('测试内容')).toBeInTheDocument();
  });

  it('应该应用自定义类名', () => {
    const { container } = render(
      <PisellGridPro className="custom-class">
        <div>内容</div>
      </PisellGridPro>
    );

    expect(container.querySelector('.pisell-grid-pro')).toHaveClass('custom-class');
  });

  it('应该应用自定义样式', () => {
    const customStyle = { background: 'red', padding: '20px' };
    const { container } = render(
      <PisellGridPro style={customStyle}>
        <div>内容</div>
      </PisellGridPro>
    );

    const element = container.querySelector('.pisell-grid-pro');
    expect(element).toHaveStyle({ background: 'red', padding: '20px' });
  });

  it('快照测试', () => {
    const { container } = render(
      <PisellGridPro>
        <div>测试内容</div>
      </PisellGridPro>
    );

    expect(container).toMatchSnapshot();
  });
});
```

### 2. Header 组件测试

```
import { render, screen } from '@testing-library/react';
import { Badge } from 'antd';
import Header from '../Header';

describe('PisellGridPro.Header', () => {
  it('应该正确渲染标题', () => {
    render(<Header title="测试标题" />);
    expect(screen.getByText('测试标题')).toBeInTheDocument();
  });

  it('应该渲染徽章', () => {
    render(
      <Header
        title="标题"
        badge={<Badge count={5} />}
      />
    );

    expect(document.querySelector('.ant-badge')).toBeInTheDocument();
  });

  it('应该渲染副标题', () => {
    render(
      <Header
        title="主标题"
        subTitle="副标题内容"
      />
    );

    expect(screen.getByText('副标题内容')).toBeInTheDocument();
    expect(document.querySelector('.pisell-grid-pro-header-subtitle')).toBeInTheDocument();
  });

  it('应该渲染操作按钮', () => {
    render(
      <Header
        title="标题"
        actionsList={
          <>
            <button>按钮1</button>
            <button>按钮2</button>
          </>
        }
      />
    );

    expect(screen.getByText('按钮1')).toBeInTheDocument();
    expect(screen.getByText('按钮2')).toBeInTheDocument();
    expect(document.querySelector('.pisell-grid-pro-header-actions')).toBeInTheDocument();
  });

  it('不应该渲染空的副标题', () => {
    render(<Header title="标题" />);
    expect(document.querySelector('.pisell-grid-pro-header-subtitle')).not.toBeInTheDocument();
  });

  it('不应该渲染空的徽章', () => {
    render(<Header title="标题" />);
    expect(document.querySelector('.pisell-grid-pro-header-badge')).not.toBeInTheDocument();
  });

  it('应该应用自定义类名和样式', () => {
    const { container } = render(
      <Header
        title="标题"
        className="custom-header"
        style={{ padding: '20px' }}
      />
    );

    const header = container.querySelector('.pisell-grid-pro-header');
    expect(header).toHaveClass('custom-header');
    expect(header).toHaveStyle({ padding: '20px' });
  });

  it('快照测试 - 完整配置', () => {
    const { container } = render(
      <Header
        title="完整标题"
        badge={<Badge count={10} />}
        subTitle="这是副标题"
        actionsList={
          <>
            <button>创建</button>
            <button>导出</button>
          </>
        }
      />
    );

    expect(container).toMatchSnapshot();
  });
});
```

### 3. ToolBar 组件测试

```
import { render, screen } from '@testing-library/react';
import ToolBar from '../ToolBar';

describe('PisellGridPro.ToolBar', () => {
  it('应该渲染标签页', () => {
    render(
      <ToolBar
        tabs={<div data-testid="tabs">标签页</div>}
      />
    );

    expect(screen.getByTestId('tabs')).toBeInTheDocument();
  });

  it('应该渲染顶部左右内容', () => {
    render(
      <ToolBar
        topLeft={<div>左侧内容</div>}
        topRight={<div>右侧内容</div>}
      />
    );

    expect(screen.getByText('左侧内容')).toBeInTheDocument();
    expect(screen.getByText('右侧内容')).toBeInTheDocument();
  });

  it('应该渲染底部左右内容', () => {
    render(
      <ToolBar
        bottomLeft={<div>底部左侧</div>}
        bottomRight={<div>底部右侧</div>}
      />
    );

    expect(screen.getByText('底部左侧')).toBeInTheDocument();
    expect(screen.getByText('底部右侧')).toBeInTheDocument();
  });

  it('应该同时渲染多个插槽', () => {
    render(
      <ToolBar
        tabs={<div>标签页</div>}
        topLeft={<div>顶部左侧</div>}
        topRight={<div>顶部右侧</div>}
        bottomLeft={<div>底部左侧</div>}
        bottomRight={<div>底部右侧</div>}
      />
    );

    expect(screen.getByText('标签页')).toBeInTheDocument();
    expect(screen.getByText('顶部左侧')).toBeInTheDocument();
    expect(screen.getByText('顶部右侧')).toBeInTheDocument();
    expect(screen.getByText('底部左侧')).toBeInTheDocument();
    expect(screen.getByText('底部右侧')).toBeInTheDocument();
  });

  it('空内容时不应该渲染对应行', () => {
    const { container } = render(<ToolBar />);

    expect(container.querySelectorAll('.pisell-grid-pro-toolbar-row')).toHaveLength(0);
  });

  it('只有部分内容时应该只渲染对应行', () => {
    const { container } = render(
      <ToolBar topLeft={<div>内容</div>} />
    );

    expect(container.querySelectorAll('.pisell-grid-pro-toolbar-row')).toHaveLength(1);
  });

  it('应该应用自定义类名和样式', () => {
    const { container } = render(
      <ToolBar
        className="custom-toolbar"
        style={{ padding: '10px' }}
        topLeft={<div>内容</div>}
      />
    );

    const toolbar = container.querySelector('.pisell-grid-pro-toolbar');
    expect(toolbar).toHaveClass('custom-toolbar');
    expect(toolbar).toHaveStyle({ padding: '10px' });
  });

  it('快照测试 - 完整配置', () => {
    const { container } = render(
      <ToolBar
        tabs={<div>标签页</div>}
        topLeft={<div>顶部左侧</div>}
        topRight={<div>顶部右侧</div>}
        bottomLeft={<div>底部左侧</div>}
        bottomRight={<div>底部右侧</div>}
      />
    );

    expect(container).toMatchSnapshot();
  });
});
```

### 4. DataCard 组件测试

```
import { render, screen, fireEvent } from '@testing-library/react';
import DataCard from '../DataCard';
import { StatCardItem } from '../DataCard/types';

describe('PisellGridPro.DataCard', () => {
  const mockDataItems: StatCardItem[] = [
    {
      key: '1',
      title: '总收入',
      value: '$45,231',
      compareValue: '+20%',
      trend: 'up',
    },
    {
      key: '2',
      title: '订单数',
      value: '1,234',
      compareValue: '-5',
      trend: 'down',
    },
  ];

  it('应该渲染多个统计卡片', () => {
    render(<DataCard dataItems={mockDataItems} />);

    expect(screen.getByText('总收入')).toBeInTheDocument();
    expect(screen.getByText('$45,231')).toBeInTheDocument();
    expect(screen.getByText('订单数')).toBeInTheDocument();
    expect(screen.getByText('1,234')).toBeInTheDocument();
  });

  it('空数据时不应该渲染', () => {
    const { container } = render(<DataCard dataItems={[]} />);

    expect(container.querySelector('.pisell-grid-pro-datacard')).not.toBeInTheDocument();
  });

  it('空数据时返回 null', () => {
    const { container } = render(<DataCard />);

    expect(container.firstChild).toBeNull();
  });

  it('应该显示趋势图标', () => {
    const { container } = render(<DataCard dataItems={mockDataItems} />);

    const upArrow = container.querySelector('.anticon-arrow-up');
    const downArrow = container.querySelector('.anticon-arrow-down');

    expect(upArrow).toBeInTheDocument();
    expect(downArrow).toBeInTheDocument();
  });

  it('应该应用趋势颜色类名', () => {
    const { container } = render(<DataCard dataItems={mockDataItems} />);

    const trendUp = container.querySelector('.trend-up');
    const trendDown = container.querySelector('.trend-down');

    expect(trendUp).toBeInTheDocument();
    expect(trendDown).toBeInTheDocument();
  });

  it('应该支持点击事件', () => {
    const handleClick = jest.fn();
    const dataItems: StatCardItem[] = [
      {
        key: '1',
        title: '可点击卡片',
        value: '100',
        onClick: handleClick,
      },
    ];

    render(<DataCard dataItems={dataItems} />);

    const card = screen.getByText('可点击卡片').closest('.stat-card');
    fireEvent.click(card!);

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('可点击卡片应该有对应类名', () => {
    const dataItems: StatCardItem[] = [
      {
        key: '1',
        title: '可点击卡片',
        value: '100',
        onClick: jest.fn(),
      },
    ];

    const { container } = render(<DataCard dataItems={dataItems} />);

    expect(container.querySelector('.stat-card-clickable')).toBeInTheDocument();
  });

  it('应该支持隐藏对比信息', () => {
    const dataItems: StatCardItem[] = [
      {
        key: '1',
        title: '测试',
        value: '100',
        compareValue: '+10%',
        showCompare: false,
      },
    ];

    const { container } = render(<DataCard dataItems={dataItems} />);

    expect(container.querySelector('.stat-card-compare')).not.toBeInTheDocument();
  });

  it('应该使用自定义对比前缀', () => {
    const dataItems: StatCardItem[] = [
      {
        key: '1',
        title: '测试',
        value: '100',
        compareValue: '+10%',
        comparePrefix: '较上月',
      },
    ];

    render(<DataCard dataItems={dataItems} />);

    expect(screen.getByText('较上月')).toBeInTheDocument();
  });

  it('应该应用自定义类名和样式', () => {
    const { container } = render(
      <DataCard
        dataItems={mockDataItems}
        className="custom-datacard"
        style={{ gap: '24px' }}
      />
    );

    const datacard = container.querySelector('.pisell-grid-pro-datacard');
    expect(datacard).toHaveClass('custom-datacard');
    expect(datacard).toHaveStyle({ gap: '24px' });
  });

  it('快照测试', () => {
    const { container } = render(<DataCard dataItems={mockDataItems} />);

    expect(container).toMatchSnapshot();
  });
});
```

### 5. StatCard 子组件测试

```
import { render, screen, fireEvent } from '@testing-library/react';
import StatCard from '../DataCard/components/StatCard';

describe('StatCard', () => {
  it('应该渲染基础内容', () => {
    render(
      <StatCard
        title="测试标题"
        value="1,234"
      />
    );

    expect(screen.getByText('测试标题')).toBeInTheDocument();
    expect(screen.getByText('1,234')).toBeInTheDocument();
  });

  it('应该渲染对比信息', () => {
    render(
      <StatCard
        title="标题"
        value="100"
        compareValue="+10%"
        trend="up"
      />
    );

    expect(screen.getByText('+10%')).toBeInTheDocument();
    expect(screen.getByText('v.s. pre')).toBeInTheDocument(); // 默认前缀
  });

  it('应该渲染上升趋势图标', () => {
    const { container } = render(
      <StatCard
        title="标题"
        value="100"
        compareValue="+10%"
        trend="up"
      />
    );

    expect(container.querySelector('.anticon-arrow-up')).toBeInTheDocument();
    expect(container.querySelector('.trend-up')).toBeInTheDocument();
  });

  it('应该渲染下降趋势图标', () => {
    const { container } = render(
      <StatCard
        title="标题"
        value="100"
        compareValue="-5%"
        trend="down"
      />
    );

    expect(container.querySelector('.anticon-arrow-down')).toBeInTheDocument();
    expect(container.querySelector('.trend-down')).toBeInTheDocument();
  });

  it('无趋势时不应该渲染图标', () => {
    const { container } = render(
      <StatCard
        title="标题"
        value="100"
        compareValue="+10%"
      />
    );

    expect(container.querySelector('.trend-icon')).not.toBeInTheDocument();
  });

  it('应该支持自定义对比前缀', () => {
    render(
      <StatCard
        title="标题"
        value="100"
        compareValue="+10%"
        comparePrefix="较昨日"
      />
    );

    expect(screen.getByText('较昨日')).toBeInTheDocument();
  });

  it('showCompare=false 时不应该显示对比信息', () => {
    const { container } = render(
      <StatCard
        title="标题"
        value="100"
        compareValue="+10%"
        showCompare={false}
      />
    );

    expect(container.querySelector('.stat-card-compare')).not.toBeInTheDocument();
  });

  it('应该支持点击事件', () => {
    const handleClick = jest.fn();

    render(
      <StatCard
        title="可点击"
        value="100"
        onClick={handleClick}
      />
    );

    const card = screen.getByText('可点击').closest('.stat-card');
    fireEvent.click(card!);

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('可点击时应该有对应类名', () => {
    const { container } = render(
      <StatCard
        title="可点击"
        value="100"
        onClick={jest.fn()}
      />
    );

    expect(container.querySelector('.stat-card-clickable')).toBeInTheDocument();
  });

  it('不可点击时不应该有对应类名', () => {
    const { container } = render(
      <StatCard
        title="不可点击"
        value="100"
      />
    );

    expect(container.querySelector('.stat-card-clickable')).not.toBeInTheDocument();
  });

  it('应该应用自定义类名和样式', () => {
    const { container } = render(
      <StatCard
        title="标题"
        value="100"
        className="custom-card"
        style={{ background: 'red' }}
      />
    );

    const card = container.querySelector('.stat-card');
    expect(card).toHaveClass('custom-card');
    expect(card).toHaveStyle({ background: 'red' });
  });
});
```

### 6. View 组件测试

```
import { render, screen } from '@testing-library/react';
import View from '../View';

describe('PisellGridPro.View', () => {
  it('应该正确渲染子内容', () => {
    render(
      <View>
        <div>测试内容</div>
      </View>
    );

    expect(screen.getByText('测试内容')).toBeInTheDocument();
  });

  it('应该包含 PisellScrollView', () => {
    const { container } = render(
      <View>
        <div>内容</div>
      </View>
    );

    // PisellScrollView 会渲染特定的类名或结构
    expect(container.querySelector('.pisell-grid-pro-view')).toBeInTheDocument();
  });

  it('应该应用自定义类名和样式', () => {
    const { container } = render(
      <View className="custom-view" style={{ background: '#f5f5f5' }}>
        <div>内容</div>
      </View>
    );

    const view = container.querySelector('.pisell-grid-pro-view');
    expect(view).toHaveClass('custom-view');
    expect(view).toHaveStyle({ background: '#f5f5f5' });
  });

  it('快照测试', () => {
    const { container } = render(
      <View>
        <div>测试内容</div>
      </View>
    );

    expect(container).toMatchSnapshot();
  });
});
```

### 7. Footer 组件测试

```
import { render, screen } from '@testing-library/react';
import Footer from '../Footer';

describe('PisellGridPro.Footer', () => {
  it('应该正确渲染子内容', () => {
    render(
      <Footer>
        <div>页脚内容</div>
      </Footer>
    );

    expect(screen.getByText('页脚内容')).toBeInTheDocument();
  });

  it('应该应用自定义类名和样式', () => {
    const { container } = render(
      <Footer className="custom-footer" style={{ padding: '20px' }}>
        <div>内容</div>
      </Footer>
    );

    const footer = container.querySelector('.pisell-grid-pro-footer');
    expect(footer).toHaveClass('custom-footer');
    expect(footer).toHaveStyle({ padding: '20px' });
  });

  it('快照测试', () => {
    const { container } = render(
      <Footer>
        <div>页脚内容</div>
      </Footer>
    );

    expect(container).toMatchSnapshot();
  });
});
```

## 集成测试

### 完整页面布局测试

```
import { render, screen } from '@testing-library/react';
import PisellGridPro from '../index';

describe('PisellGridPro - 集成测试', () => {
  it('应该正确渲染完整页面布局', () => {
    render(
      <PisellGridPro>
        <PisellGridPro.Header title="页面标题" />
        <PisellGridPro.ToolBar topLeft={<div>工具栏</div>} />
        <PisellGridPro.DataCard
          dataItems={[
            { key: '1', title: '统计1', value: '100' },
            { key: '2', title: '统计2', value: '200' },
          ]}
        />
        <PisellGridPro.View>
          <div>主内容</div>
        </PisellGridPro.View>
        <PisellGridPro.Footer>
          <div>页脚</div>
        </PisellGridPro.Footer>
      </PisellGridPro>
    );

    // 验证所有组件都正确渲染
    expect(screen.getByText('页面标题')).toBeInTheDocument();
    expect(screen.getByText('工具栏')).toBeInTheDocument();
    expect(screen.getByText('统计1')).toBeInTheDocument();
    expect(screen.getByText('统计2')).toBeInTheDocument();
    expect(screen.getByText('主内容')).toBeInTheDocument();
    expect(screen.getByText('页脚')).toBeInTheDocument();
  });

  it('应该支持部分子组件', () => {
    render(
      <PisellGridPro>
        <PisellGridPro.Header title="简单页面" />
        <PisellGridPro.View>
          <div>只有标题和内容</div>
        </PisellGridPro.View>
      </PisellGridPro>
    );

    expect(screen.getByText('简单页面')).toBeInTheDocument();
    expect(screen.getByText('只有标题和内容')).toBeInTheDocument();
  });

  it('应该保持正确的 DOM 结构', () => {
    const { container } = render(
      <PisellGridPro>
        <PisellGridPro.Header title="标题" />
        <PisellGridPro.View>内容</PisellGridPro.View>
      </PisellGridPro>
    );

    const gridPro = container.querySelector('.pisell-grid-pro');
    expect(gridPro?.children.length).toBe(2); // Header + View
  });

  it('快照测试 - 完整布局', () => {
    const { container } = render(
      <PisellGridPro>
        <PisellGridPro.Header
          title="完整页面"
          subTitle="副标题"
          actionsList={<button>操作</button>}
        />
        <PisellGridPro.ToolBar
          tabs={<div>标签页</div>}
          topLeft={<div>左侧</div>}
          topRight={<div>右侧</div>}
        />
        <PisellGridPro.DataCard
          dataItems={[
            { key: '1', title: '统计', value: '100' },
          ]}
        />
        <PisellGridPro.View>
          <div>主内容区域</div>
        </PisellGridPro.View>
        <PisellGridPro.Footer>
          <div>分页</div>
        </PisellGridPro.Footer>
      </PisellGridPro>
    );

    expect(container).toMatchSnapshot();
  });
});
```

## 样式测试

### CSS 类名测试

```
describe('PisellGridPro - 样式测试', () => {
  it('主容器应该有正确的类名', () => {
    const { container } = render(
      <PisellGridPro>
        <div>内容</div>
      </PisellGridPro>
    );

    const gridPro = container.querySelector('.pisell-grid-pro');
    expect(gridPro).toBeInTheDocument();
    expect(gridPro).toHaveClass('pisell-grid-pro');
  });

  it('Header 应该有正确的类名结构', () => {
    const { container } = render(
      <PisellGridPro.Header
        title="标题"
        badge={<div>徽章</div>}
        subTitle="副标题"
      />
    );

    expect(container.querySelector('.pisell-grid-pro-header')).toBeInTheDocument();
    expect(container.querySelector('.pisell-grid-pro-header-left')).toBeInTheDocument();
    expect(container.querySelector('.pisell-grid-pro-header-title-wrapper')).toBeInTheDocument();
    expect(container.querySelector('.pisell-grid-pro-header-title')).toBeInTheDocument();
    expect(container.querySelector('.pisell-grid-pro-header-badge')).toBeInTheDocument();
    expect(container.querySelector('.pisell-grid-pro-header-subtitle')).toBeInTheDocument();
  });

  it('DataCard 应该有 Grid 布局', () => {
    const { container } = render(
      <PisellGridPro.DataCard
        dataItems={[
          { key: '1', title: '测试', value: '100' },
        ]}
      />
    );

    const datacard = container.querySelector('.pisell-grid-pro-datacard');
    const styles = window.getComputedStyle(datacard!);

    expect(styles.display).toBe('grid');
  });
});
```

## 运行测试

### 运行所有测试

```bash
npm test
```

### 运行特定文件的测试

```bash
npm test PisellGridPro.test.tsx
```

### 运行特定组件的测试

```bash
npm test -- --testNamePattern="Header"
```

### 生成覆盖率报告

```bash
npm test -- --coverage
```

### Watch 模式

```bash
npm test -- --watch
```

### 更新快照

```bash
npm test -- -u
```

## 测试覆盖率要求

| 类型       | 要求  |
| ---------- | ----- |
| 语句覆盖率 | ≥ 80% |
| 分支覆盖率 | ≥ 80% |
| 函数覆盖率 | ≥ 80% |
| 行覆盖率   | ≥ 80% |

## 测试清单

开发新组件或修改现有组件时，请确保包含以下测试：

### 基础渲染测试

- [ ] 组件正确渲染
- [ ] 子内容正确显示
- [ ] 快照测试

### Props 测试

- [ ] 所有 props 正确应用
- [ ] 可选 props 默认值正确
- [ ] Props 类型正确

### 条件渲染测试

- [ ] 条件内容正确显示/隐藏
- [ ] 空状态正确处理
- [ ] 边界情况处理

### 交互测试

- [ ] 点击事件正确触发
- [ ] 回调函数正确调用
- [ ] 事件参数正确传递

### 样式测试

- [ ] 基础类名正确应用
- [ ] 自定义类名正确合并
- [ ] 内联样式正确应用
- [ ] 条件类名正确添加

### 集成测试

- [ ] 多个子组件协同工作
- [ ] 完整布局正确渲染
- [ ] Props 正确传递给子组件

## 测试最佳实践

### 1. 使用语义化查询

```
// Good: 使用 screen.getByText
expect(screen.getByText('标题')).toBeInTheDocument();

// Bad: 使用 querySelector
expect(container.querySelector('.title')).toHaveTextContent('标题');
```

### 2. 测试用户行为

```
// Good: 模拟用户点击
fireEvent.click(screen.getByText('按钮'));

// Bad: 直接调用回调
onClick();
```

### 3. 避免测试实现细节

```
// Good: 测试渲染结果
expect(screen.getByText('内容')).toBeInTheDocument();

// Bad: 测试内部状态
expect(component.state.value).toBe('内容');
```

### 4. 使用有意义的断言

```
// Good: 明确的断言
expect(screen.getByText('标题')).toBeInTheDocument();
expect(container.querySelector('.header')).toHaveClass('active');

// Bad: 模糊的断言
expect(screen.getByText('标题')).toBeTruthy();
```

### 5. 保持测试独立

```
// Good: 每个测试独立
it('test 1', () => {
  render(<Component />);
  // ...
});

it('test 2', () => {
  render(<Component />);
  // ...
});

// Bad: 测试之间有依赖
let component;
it('test 1', () => {
  component = render(<Component />);
});
it('test 2', () => {
  // 依赖 test 1 的 component
});
```

### 6. 清理副作用

```
afterEach(() => {
  jest.clearAllMocks();
  cleanup(); // @testing-library/react 提供
});
```

## 常见测试问题

### 1. 快照测试失败

**问题**：快照不匹配

**解决**：

```bash
# 检查变更是否符合预期
npm test -- -u  # 更新快照
```

### 2. 异步内容不显示

**问题**：异步加载的内容测试失败

**解决**：

```
// 使用 waitFor
await waitFor(() => {
  expect(screen.getByText('异步内容')).toBeInTheDocument();
});
```

### 3. CSS 样式测试不准确

**问题**：getComputedStyle 返回空字符串

**解决**：

```
// 在 jsdom 环境中，某些 CSS 属性可能无法正确计算
// 改为测试类名
expect(element).toHaveClass('expected-class');
```

### 4. 事件处理器未触发

**问题**：fireEvent 后回调未调用

**解决**：

```
// 确保选择器正确
const button = screen.getByRole('button');
fireEvent.click(button);

// 或使用 userEvent (更接近真实用户行为)
import userEvent from '@testing-library/user-event';
await userEvent.click(button);
```

## 持续集成

### GitHub Actions 配置

```yaml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install dependencies
        run: npm install

      - name: Run tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v2
        with:
          files: ./coverage/lcov.info
```

## 总结

良好的测试覆盖可以：

1. **确保代码质量**：及时发现 bug
2. **防止回归**：避免修改破坏现有功能
3. **提升信心**：放心重构和优化
4. **作为文档**：测试用例展示组件用法

编写测试时记住：

- 测试行为，不测试实现
- 保持测试简单明了
- 及时更新测试
- 追求有意义的覆盖率，而非 100%覆盖率
