# 前端测试技术文档

## 1. 测试架构概述

### 1.1 测试金字塔

```
        /\
       /  \     E2E测试
      /    \    (Playwright, 最少)
     /------\
    /        \   集成测试
   /          \  (RTL, 适中)
  /____________\
 /              \ 单元测试
/________________\ (Vitest, 最多)
```

### 1.2 测试层级

| 测试类型 | 覆盖范围 | 比例 | 运行速度 | 工具 |
|---------|---------|------|---------|------|
| 单元测试 | 函数、组件、Hooks | 70% | 快 (< 1s) | Vitest |
| 集成测试 | 页面、路由、API交互 | 20% | 中 (< 5s) | React Testing Library |
| E2E测试 | 完整用户流程 | 10% | 慢 (< 30s) | Playwright |

## 2. 测试工具栈

### 2.1 单元测试

**Vitest**
- 快速的单元测试框架
- 与Vite深度集成
- 兼容Jest API
- 支持TypeScript

**安装**
```bash
npm install -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
```

### 2.2 集成测试

**React Testing Library (RTL)**
- 测试React组件
- 关注用户行为而非实现细节
- 可访问性优先

### 2.3 E2E测试

**Playwright**
- 跨浏览器E2E测试
- 快速、可靠
- 支持并行执行

**安装**
```bash
npm install -D @playwright/test
npx playwright install
```

### 2.4 测试覆盖率

**Vitest覆盖率**
```bash
npm run test:coverage
```

**覆盖率目标**
- 语句覆盖率: > 80%
- 分支覆盖率: > 75%
- 函数覆盖率: > 80%
- 行覆盖率: > 80%

## 3. 测试配置

### 3.1 Vitest配置

```javascript
// vitest.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './tests/setup.js',
    css: true,
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'tests/',
        '**/*.config.js',
        '**/*.config.jsx',
        '**/dist/',
        '**/build/',
      ],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 75,
        statements: 80,
      },
    },
    include: ['src/**/*.{test,spec}.{js,jsx}'],
    testTimeout: 10000,
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});
```

### 3.2 测试环境设置

```javascript
// tests/setup.js
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';

// 每个测试后清理
afterEach(() => {
  cleanup();
});

// 模拟window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
});

// 模拟localStorage
const localStorageMock = (() => {
  let store = {};
  return {
    getItem: key => store[key] || null,
    setItem: (key, value) => (store[key] = value.toString()),
    removeItem: key => delete store[key],
    clear: () => (store = {}),
  };
})();

Object.defineProperty(window, 'localStorage', {
  value: localStorageMock,
});
```

### 3.3 Playwright配置

```javascript
// playwright.config.js
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/results.json' }],
  ],
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
});
```

## 4. 单元测试

### 4.1 测试工具函数

```javascript
// src/utils/format.test.js
import { describe, it, expect } from 'vitest';
import { formatMoney, formatPercentage, formatDate } from './format';

describe('formatMoney', () => {
  it('应该正确格式化金额', () => {
    expect(formatMoney(1234.56)).toBe('1,234.56');
    expect(formatMoney(10000)).toBe('10,000.00');
  });

  it('应该处理小数', () => {
    expect(formatMoney(100.5)).toBe('100.50');
    expect(formatMoney(100.123)).toBe('100.12');
  });

  it('应该处理零', () => {
    expect(formatMoney(0)).toBe('0.00');
  });

  it('应该处理负数', () => {
    expect(formatMoney(-1234.56)).toBe('-1,234.56');
  });
});

describe('formatPercentage', () => {
  it('应该正确格式化百分比', () => {
    expect(formatPercentage(0.5)).toBe('50.00%');
    expect(formatPercentage(1)).toBe('100.00%');
  });

  it('应该保留两位小数', () => {
    expect(formatPercentage(0.6666)).toBe('66.66%');
  });
});

describe('formatDate', () => {
  it('应该正确格式化日期', () => {
    expect(formatDate('2026-01-25')).toBe('2026-01-25');
    expect(formatDate(new Date('2026-01-25'))).toBe('2026-01-25');
  });
});
```

### 4.2 测试自定义Hooks

```javascript
// src/hooks/useAuth.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { AuthProvider, useAuth } from '../contexts/AuthContext';

describe('useAuth Hook', () => {
  beforeEach(() => {
    // 清理localStorage
    localStorage.clear();
  });

  it('初始状态应该是未认证', () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    expect(result.current.isAuthenticated).toBe(false);
    expect(result.current.user).toBe(null);
    expect(result.current.token).toBe(null);
  });

  it('应该正确处理登录', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    await act(async () => {
      await result.current.login('admin', 'password123');
    });

    await waitFor(() => {
      expect(result.current.isAuthenticated).toBe(true);
      expect(result.current.user).toBeTruthy();
      expect(result.current.token).toBeTruthy();
    });
  });

  it('应该正确处理登出', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    // 先登录
    await act(async () => {
      await result.current.login('admin', 'password123');
    });

    // 然后登出
    await act(async () => {
      result.current.logout();
    });

    await waitFor(() => {
      expect(result.current.isAuthenticated).toBe(false);
      expect(result.current.user).toBe(null);
      expect(result.current.token).toBe(null);
    });
  });

  it('hasRole应该正确检查角色', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider,
    });

    await act(async () => {
      await result.current.login('admin', 'password123');
    });

    await waitFor(() => {
      expect(result.current.hasRole(['admin'])).toBe(true);
      expect(result.current.hasRole(['market'])).toBe(false);
      expect(result.current.hasRole(['admin', 'market'])).toBe(true);
    });
  });
});
```

### 4.3 测试API服务

```javascript
// src/services/project.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { projectAPI } from './project';
import api from './api';

vi.mock('./api');

describe('projectAPI', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getList', () => {
    it('应该调用正确的API', async () => {
      const mockResponse = {
        items: [{ id: 1, name: '项目1' }],
        total: 1,
      };
      api.get.mockResolvedValue(mockResponse);

      const params = { page: 1, page_size: 10 };
      const result = await projectAPI.getList(params);

      expect(api.get).toHaveBeenCalledWith('/projects', { params });
      expect(result).toEqual(mockResponse);
    });
  });

  describe('getDetail', () => {
    it('应该获取项目详情', async () => {
      const mockProject = { id: 1, name: '项目1' };
      api.get.mockResolvedValue(mockProject);

      const result = await projectAPI.getDetail(1);

      expect(api.get).toHaveBeenCalledWith('/projects/1');
      expect(result).toEqual(mockProject);
    });
  });

  describe('create', () => {
    it('应该创建新项目', async () => {
      const mockProject = { id: 2, name: '新项目' };
      api.post.mockResolvedValue(mockProject);

      const data = { name: '新项目', contract_amount: 100 };
      const result = await projectAPI.create(data);

      expect(api.post).toHaveBeenCalledWith('/projects', data);
      expect(result).toEqual(mockProject);
    });
  });

  describe('update', () => {
    it('应该更新项目', async () => {
      const mockProject = { id: 1, name: '更新项目' };
      api.put.mockResolvedValue(mockProject);

      const data = { name: '更新项目' };
      const result = await projectAPI.update(1, data);

      expect(api.put).toHaveBeenCalledWith('/projects/1', data);
      expect(result).toEqual(mockProject);
    });
  });

  describe('delete', () => {
    it('应该删除项目', async () => {
      api.delete.mockResolvedValue({});

      await projectAPI.delete(1);

      expect(api.delete).toHaveBeenCalledWith('/projects/1');
    });
  });
});
```

## 5. 集成测试

### 5.1 测试页面组件

```javascript
// src/pages/Login/Login.test.jsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Login from './Login';

// 模拟authAPI
vi.mock('../../services/auth', () => ({
  authAPI: {
    login: vi.fn(),
  },
}));

// 模拟useAuth Hook
vi.mock('../../hooks/useAuth', () => ({
  useAuth: () => ({
    login: vi.fn(),
    loading: false,
  }),
}));

describe('Login Page', () => {
  const renderLogin = () => {
    return render(
      <BrowserRouter>
        <Login />
      </BrowserRouter>
    );
  };

  it('应该渲染登录表单', () => {
    renderLogin();
    expect(screen.getByText('海洋项目管理系统')).toBeInTheDocument();
    expect(screen.getByLabelText(/用户名/)).toBeInTheDocument();
    expect(screen.getByLabelText(/密码/)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /登录/ })).toBeInTheDocument();
  });

  it('应该显示必填字段验证错误', async () => {
    renderLogin();

    const submitButton = screen.getByRole('button', { name: /登录/ });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText(/请输入用户名/)).toBeInTheDocument();
      expect(screen.getByText(/请输入密码/)).toBeInTheDocument();
    });
  });

  it('应该成功登录', async () => {
    const { login } = useAuth();
    login.mockResolvedValue({
      user: { id: 1, username: 'admin' },
      token: 'mock-token',
    });

    renderLogin();

    const usernameInput = screen.getByLabelText(/用户名/);
    const passwordInput = screen.getByLabelText(/密码/);
    const submitButton = screen.getByRole('button', { name: /登录/ });

    fireEvent.change(usernameInput, { target: { value: 'admin' } });
    fireEvent.change(passwordInput, { target: { value: 'password' } });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(login).toHaveBeenCalledWith('admin', 'password');
    });
  });

  it('应该显示登录失败错误', async () => {
    const { login } = useAuth();
    login.mockRejectedValue(new Error('用户名或密码错误'));

    renderLogin();

    const usernameInput = screen.getByLabelText(/用户名/);
    const passwordInput = screen.getByLabelText(/密码/);
    const submitButton = screen.getByRole('button', { name: /登录/ });

    fireEvent.change(usernameInput, { target: { value: 'admin' } });
    fireEvent.change(passwordInput, { target: { value: 'wrong' } });
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(screen.getByText(/用户名或密码错误/)).toBeInTheDocument();
    });
  });
});
```

### 5.2 测试Dashboard页面

```javascript
// src/pages/Dashboard/Dashboard.test.jsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import Dashboard from './Dashboard';

// 模拟API
vi.mock('../../services/project', () => ({
  projectAPI: {
    getList: vi.fn(),
  },
}));

vi.mock('../../services/statistics', () => ({
  statisticsAPI: {
    getBasicStatistics: vi.fn(),
  },
}));

describe('Dashboard Page', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('应该渲染统计卡片', async () => {
    const mockStatistics = {
      total_count: 100,
      total_contract_amount: 10000,
    };

    const { statisticsAPI } = await import('../../services/statistics');
    statisticsAPI.getBasicStatistics.mockResolvedValue(mockStatistics);

    render(
      <BrowserRouter>
        <Dashboard />
      </BrowserRouter>
    );

    await waitFor(() => {
      expect(screen.getByText(/项目总数/)).toBeInTheDocument();
      expect(screen.getByText(/100/)).toBeInTheDocument();
    });
  });

  it('应该渲染图表', async () => {
    const mockStatistics = {
      total_count: 100,
    };

    const { statisticsAPI } = await import('../../services/statistics');
    statisticsAPI.getBasicStatistics.mockResolvedValue(mockStatistics);

    render(
      <BrowserRouter>
        <Dashboard />
      </BrowserRouter>
    );

    await waitFor(() => {
      expect(screen.getByText(/项目趋势/)).toBeInTheDocument();
    });
  });
});
```

## 6. E2E测试

### 6.1 登录流程测试

```javascript
// tests/e2e/login.spec.js
import { test, expect } from '@playwright/test';

test.describe('登录流程', () => {
  test('应该成功登录并跳转到仪表盘', async ({ page }) => {
    // 访问登录页
    await page.goto('http://localhost:3000/login');

    // 输入用户名和密码
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'password123');

    // 点击登录按钮
    await page.click('button[type="submit"]');

    // 验证跳转到仪表盘
    await expect(page).toHaveURL('http://localhost:3000/dashboard');

    // 验证用户信息显示
    await expect(page.locator('.user-info')).toContainText('系统管理员');
  });

  test('应该显示登录失败错误', async ({ page }) => {
    await page.goto('http://localhost:3000/login');

    // 输入错误密码
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'wrongpassword');
    await page.click('button[type="submit"]');

    // 验证错误提示
    await expect(page.locator('.error-message')).toContainText('用户名或密码错误');
  });

  test('应该验证必填字段', async ({ page }) => {
    await page.goto('http://localhost:3000/login');

    // 直接点击登录按钮
    await page.click('button[type="submit"]');

    // 验证错误提示
    await expect(page.locator('text=请输入用户名')).toBeVisible();
    await expect(page.locator('text=请输入密码')).toBeVisible();
  });
});
```

### 6.2 项目管理测试

```javascript
// tests/e2e/projects.spec.js
import { test, expect } from '@playwright/test';

test.describe('项目管理', () => {
  test.beforeEach(async ({ page }) => {
    // 登录
    await page.goto('http://localhost:3000/login');
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');
  });

  test('应该显示项目列表', async ({ page }) => {
    // 导航到项目列表
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 验证表格显示
    await expect(page.locator('table')).toBeVisible();
    await expect(page.locator('th:has-text("项目编号")')).toBeVisible();
    await expect(page.locator('th:has-text("项目名称")')).toBeVisible();
  });

  test('应该创建新项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 点击新建项目按钮
    await page.click('button:has-text("新建项目")');

    // 填写表单
    await page.fill('input[name="project_no"]', 'PRJ001');
    await page.fill('input[name="name"]', '测试项目');
    await page.selectOption('select[name="engineering_type"]', '基建');
    await page.fill('input[name="contract_amount"]', '100');

    // 提交表单
    await page.click('button:has-text("保存")');

    // 验证成功提示
    await expect(page.locator('.success-message')).toContainText('项目创建成功');

    // 验证项目出现在列表中
    await expect(page.locator('table')).toContainText('PRJ001');
  });

  test('应该搜索项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 输入搜索关键词
    await page.fill('input[placeholder*="搜索"]', '测试项目');
    await page.press('input[placeholder*="搜索"]', 'Enter');

    // 验证搜索结果
    await expect(page.locator('table')).toContainText('测试项目');
  });

  test('应该筛选项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 选择工程类别
    await page.click('select[name="engineering_type"]');
    await page.click('option:has-text("基建")');

    // 验证筛选结果
    await expect(page.locator('table')).toBeVisible();
  });

  test('应该删除项目', async ({ page }) => {
    await page.click('text=项目管理');
    await page.waitForURL('**/projects');

    // 点击删除按钮
    await page.click('button:has-text("删除")');

    // 确认删除
    await page.click('button:has-text("确定")');

    // 验证成功提示
    await expect(page.locator('.success-message')).toContainText('项目删除成功');
  });
});
```

### 6.3 用户管理测试

```javascript
// tests/e2e/users.spec.js
import { test, expect } from '@playwright/test';

test.describe('用户管理', () => {
  test.beforeEach(async ({ page }) => {
    // 管理员登录
    await page.goto('http://localhost:3000/login');
    await page.fill('input[name="username"]', 'admin');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/dashboard');
  });

  test('应该显示用户列表', async ({ page }) => {
    await page.click('text=用户管理');
    await page.waitForURL('**/users');

    await expect(page.locator('table')).toBeVisible();
    await expect(page.locator('th:has-text("用户名")')).toBeVisible();
  });

  test('应该创建新用户', async ({ page }) => {
    await page.click('text=用户管理');
    await page.waitForURL('**/users');

    await page.click('button:has-text("新建用户")');

    // 填写用户信息
    await page.fill('input[name="username"]', 'testuser');
    await page.fill('input[name="password"]', 'password123');
    await page.fill('input[name="real_name"]', '测试用户');
    await page.selectOption('select[name="department"]', '管理部');
    await page.selectOption('select[name="role"]', 'admin');

    await page.click('button:has-text("保存")');

    await expect(page.locator('.success-message')).toContainText('用户创建成功');
  });

  test('应该重置用户密码', async ({ page }) => {
    await page.click('text=用户管理');
    await page.waitForURL('**/users');

    await page.click('button:has-text("重置密码")');

    await page.fill('input[name="new_password"]', 'newpassword123');
    await page.click('button:has-text("确定")');

    await expect(page.locator('.success-message')).toContainText('密码重置成功');
  });
});
```

## 7. 测试最佳实践

### 7.1 单元测试最佳实践

1. **测试名称应该描述清楚**
   ```javascript
   // 好的测试名称
   it('应该正确格式化金额为千分位分隔', () => {})

   // 不好的测试名称
   it('测试formatMoney函数', () => {})
   ```

2. **遵循AAA模式**
   ```javascript
   it('应该正确格式化金额', () => {
     // Arrange (准备)
     const amount = 1234.56;

     // Act (执行)
     const result = formatMoney(amount);

     // Assert (断言)
     expect(result).toBe('1,234.56');
   });
   ```

3. **测试边界情况**
   ```javascript
   it('应该处理边界值', () => {
     expect(formatMoney(0)).toBe('0.00');
     expect(formatMoney(0.005)).toBe('0.01');
     expect(formatMoney(9999999999)).toBe('9,999,999,999.00');
   });
   ```

4. **避免测试实现细节**
   ```javascript
   // 不好的测试：测试内部状态
   it('应该设置loading状态为true', () => {
     render(<Login />);
     fireEvent.click(screen.getByText('登录'));
     expect(component.state.loading).toBe(true);
   });

   // 好的测试：测试用户可见行为
   it('应该显示加载状态', () => {
     render(<Login />);
     fireEvent.click(screen.getByText('登录'));
     expect(screen.getByRole('button')).toBeDisabled();
   });
   ```

### 7.2 集成测试最佳实践

1. **关注用户行为**
   ```javascript
   // 好的测试：模拟用户交互
   render(<ProjectForm />);
   fireEvent.change(screen.getByLabelText('项目名称'), {
     target: { value: '新项目' },
   });
   fireEvent.click(screen.getByText('保存'));

   // 不好的测试：直接设置props
   render(<ProjectForm value="新项目" />);
   ```

2. **使用合适的查询方法**
   ```javascript
   // 优先级从高到低
   screen.getByRole('button', { name: '保存' });
   screen.getByLabelText('项目名称');
   screen.getByPlaceholderText('请输入项目名称');
   screen.getByText('保存');
   screen.getByTestId('submit-button'); // 最后选择
   ```

3. **等待异步操作完成**
   ```javascript
   it('应该提交表单', async () => {
     render(<ProjectForm />);
     fireEvent.click(screen.getByText('保存'));

     await waitFor(() => {
       expect(screen.getByText('保存成功')).toBeInTheDocument();
     });
   });
   ```

### 7.3 E2E测试最佳实践

1. **使用Page Object模式**
   ```javascript
   // pages/ProjectListPage.js
   class ProjectListPage {
     constructor(page) {
       this.page = page;
     }

     async goto() {
       await this.page.goto('/projects');
     }

     async createProject(data) {
       await this.page.click('button:has-text("新建项目")');
       await this.page.fill('input[name="name"]', data.name);
       await this.page.click('button:has-text("保存")');
     }

     async getProjectRows() {
       return this.page.locator('table tbody tr');
     }
   }

   // 测试中使用
   test('应该创建项目', async ({ page }) => {
     const projectPage = new ProjectListPage(page);
     await projectPage.goto();
     await projectPage.createProject({ name: '测试项目' });

     const rows = await projectPage.getProjectRows();
     await expect(rows).toHaveCount(1);
   });
   ```

2. **避免硬编码选择器**
   ```javascript
   // 好的
   await page.click('button:has-text("保存")');
   await page.click('[data-testid="save-button"]');

   // 不好的
   await page.click('.btn-primary.btn-lg');
   await page.click('#submit-btn-12345');
   ```

3. **添加合理的等待**
   ```javascript
   // 好的：使用自动等待
   await page.waitForSelector('.success-message');

   // 不好：固定延迟
   await page.waitForTimeout(1000);
   ```

## 8. 测试命令

```bash
# 运行所有单元测试
npm run test

# 运行测试并监听文件变化
npm run test:watch

# 运行测试并生成覆盖率报告
npm run test:coverage

# 运行E2E测试
npm run test:e2e

# 运行E2E测试并打开UI
npm run test:e2e:ui

# 运行E2E测试（特定浏览器）
npx playwright test --project=chromium
```

## 9. CI/CD集成

### 9.1 GitHub Actions配置

```yaml
# .github/workflows/test.yml
name: Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

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

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm run test

      - name: Run E2E tests
        run: npm run test:e2e

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/coverage-final.json
```

## 10. 调试测试

### 10.1 调试单元测试

```javascript
// 使用debug()
test('调试测试', () => {
  debug();
  // 这会暂停测试，让你在浏览器中检查DOM
});
```

### 10.2 调试E2E测试

```javascript
// 使用Playwright Inspector
test('调试E2E测试', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await page.pause(); // 暂停执行，打开Inspector

  await page.click('button');
});
```

---

**文档维护**: 前端程序员
**文档类型**: 前端测试技术文档
**最后更新**: 2026-01-25
