Răsfoiți Sursa

docs: 添加故事010.005补充测试覆盖度并更新史诗010文档

- 新增故事010.005:补充统一广告管理UI包测试覆盖度
  * 目标:测试覆盖率从~60%提升到70%+
  * 新增24+测试用例(API错误处理、表单验证、分页、交互)
- 更新史诗010文档:
  * Story 2状态更新为已完成(Ready for Review)
  * 添加Story 5(010.005)详情
  * 原Story 5重新编号为Story 6

🤖 Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 2 săptămâni în urmă
părinte
comite
ab479880b6
2 a modificat fișierele cu 448 adăugiri și 7 ștergeri
  1. 42 7
      docs/prd/epic-010-unified-ad-management.md
  2. 406 0
      docs/stories/010.005.story.md

+ 42 - 7
docs/prd/epic-010-unified-ad-management.md

@@ -9,6 +9,8 @@
 | 1.2 | 2026-01-03 | 添加故事010.003:修复路由路径规范问题 | James (Claude Code) |
 | 1.3 | 2026-01-03 | 添加故事010.004:修复路由参数类型规范问题 | James (Claude Code) |
 | 1.4 | 2026-01-03 | 完成故事010.004:修复路由参数类型规范问题 | James (Claude Code) |
+| 1.5 | 2026-01-03 | 更新故事010.002状态为Ready for Review | James (Claude Code) |
+| 1.6 | 2026-01-03 | 添加故事010.005:补充测试覆盖度 | James (Claude Code) |
 
 ## 史诗目标
 
@@ -71,18 +73,22 @@
 **测试覆盖**: 57个测试全部通过(23个单元测试 + 34个集成测试)
 **相关文件**: `docs/stories/010.001.story.md`
 
-### Story 2: 创建统一广告管理UI
+### Story 2: 创建统一广告管理UI ✅ 已完成
 
 **标题**: 创建统一广告管理UI包 (unified-advertisement-management-ui)
 
 **描述**: 复制单租户广告管理UI并改造,API端点指向统一模块
 
 **任务**:
-- [ ] 创建UI包结构
-- [ ] 实现广告管理组件(列表、创建、编辑、删除)
-- [ ] 实现广告类型管理组件
-- [ ] 创建API客户端(指向统一模块端点)
-- [ ] 编写组件测试
+- [x] 创建UI包结构
+- [x] 实现广告管理组件(列表、创建、编辑、删除)
+- [x] 实现广告类型管理组件
+- [x] 创建API客户端(指向统一模块端点)
+- [x] 编写组件测试
+
+**完成日期**: 2026-01-03
+**测试覆盖**: 13个集成测试全部通过
+**相关文件**: `docs/stories/010.002.story.md`
 
 ### Story 3: 修复路由路径规范问题 ✅ 已完成
 
@@ -141,7 +147,36 @@
 **测试结果**: 57/57 测试通过
 **相关文件**: `docs/stories/010.004.story.md`
 
-### Story 5: Web集成和Server模块替换
+### Story 5: 补充测试覆盖度
+
+**标题**: 补充统一广告管理UI包测试覆盖度
+
+**描述**: 为统一广告管理UI包补充缺失的测试场景,提升测试覆盖率到70%以上,确保代码质量和稳定性。
+
+**背景说明**:
+- 故事010.002实施时完成了基础的CRUD测试(13个测试通过)
+- 但存在以下测试场景未覆盖:
+  - API错误处理(网络失败、服务器错误、业务错误)
+  - 表单验证失败(必填字段、格式验证、长度限制)
+  - 分页功能(页码切换、边界条件)
+  - 编辑表单状态切换、选择器交互、图片选择器交互
+- 当前覆盖率约60-70%,需要补充测试达到70%以上
+
+**任务**:
+- [ ] 创建API错误处理测试(网络、500、400/404/409)
+- [ ] 创建表单验证测试(必填字段、格式、长度)
+- [ ] 创建分页功能测试(页码切换、边界条件)
+- [ ] 创建编辑表单状态切换测试
+- [ ] 创建广告类型选择器交互测试
+- [ ] 创建图片选择器交互测试
+- [ ] 更新测试覆盖率配置(阈值70%)
+- [ ] 代码质量检查(覆盖率达标、类型检查)
+
+**预计新增测试**: 24+ 个测试用例
+**目标覆盖率**: 70%+ (statements/branches/functions/lines)
+**相关文件**: `docs/stories/010.005.story.md`
+
+### Story 6: Web集成和Server模块替换
 
 **标题**: 集成到租户后台、移除admin后台广告管理、Server切换模块
 

+ 406 - 0
docs/stories/010.005.story.md

@@ -0,0 +1,406 @@
+# Story 010.005: 补充统一广告管理UI包测试覆盖度
+
+## Status
+Draft
+
+## Story
+
+**As a** 开发者,
+**I want** 为统一广告管理UI包补充缺失的测试场景,
+**so that** 可以达到更高的测试覆盖率,确保代码质量和稳定性。
+
+## Acceptance Criteria
+
+1. 补充 API 错误处理测试(网络失败、服务器错误、业务错误)
+2. 补充表单验证失败测试(必填字段、格式验证、长度限制)
+3. 补充分页功能测试(页码切换、每页数量变化、边界条件)
+4. 补充编辑表单的状态切换测试(广告类型状态开关)
+5. 补充广告类型选择器交互测试(打开、选择、取消)
+6. 补充图片选择器交互测试(选择图片、清除选择)
+7. 所有测试使用 `data-testid` 进行可靠选择
+8. 测试覆盖率达到 70% 以上
+
+## Tasks / Subtasks
+
+- [ ] **任务1: 创建 API 错误处理测试** (AC: 1, 7)
+  - [ ] 创建 `tests/integration/error-handling.integration.test.tsx`
+  - [ ] 测试网络失败场景(模拟 fetch 错误)
+  - [ ] 测试服务器错误(500、503)
+  - [ ] 测试业务错误(400、404、409)
+  - [ ] 验证错误消息正确显示给用户
+  - [ ] 验证 toast.error 被正确调用
+
+- [ ] **任务2: 创建表单验证测试** (AC: 2, 7)
+  - [ ] 在现有测试文件中添加表单验证测试套件
+  - [ ] 测试广告创建表单的必填字段验证(title、typeId、code)
+  - [ ] 测试广告类型创建表单的必填字段验证(name、code)
+  - [ ] 测试字段长度限制(title最多30字符、code最多20字符)
+  - [ ] 测试格式验证(URL格式、code格式)
+  - [ ] 验证表单错误消息正确显示
+
+- [ ] **任务3: 创建分页功能测试** (AC: 3, 7)
+  - [ ] 在现有测试文件中添加分页测试套件
+  - [ ] 测试页码切换(下一页、上一页)
+  - [ ] 测试每页数量变化
+  - [ ] 测试边界条件(第一页、最后一页)
+  - [ ] 验证分页参数正确传递给 API
+
+- [ ] **任务4: 创建编辑表单状态切换测试** (AC: 4, 7)
+  - [ ] 在 `unified-advertisement-type-management.integration.test.tsx` 添加测试
+  - [ ] 测试编辑表单中的状态开关(Switch 组件)
+  - [ ] 验证状态值正确传递给 API
+
+- [ ] **任务5: 创建广告类型选择器交互测试** (AC: 5, 7)
+  - [ ] 创建 `tests/components/UnifiedAdvertisementTypeSelector.test.tsx`
+  - [ ] 测试选择器打开/关闭
+  - [ ] 测试选择类型选项
+  - [ ] 测试取消选择
+  - [ ] 测试空选项显示
+  - [ ] 测试默认值处理
+
+- [ ] **任务6: 创建图片选择器交互测试** (AC: 6, 7)
+  - [ ] 在现有测试中添加 FileSelector 交互测试
+  - [ ] 测试选择图片功能
+  - [ ] 测试清除选择功能
+  - [ ] 测试图片预览显示
+  - [ ] 验证 imageFileId 正确传递
+
+- [ ] **任务7: 更新测试覆盖率配置** (AC: 8)
+  - [ ] 更新 `vitest.config.ts` 配置覆盖率收集
+  - [ ] 配置覆盖率阈值(statements: 70, branches: 65, functions: 70, lines: 70)
+  - [ ] 修复版本兼容性问题(vitest 与 @vitest/coverage-v8 版本匹配)
+
+- [ ] **任务8: 代码质量检查** (AC: 8)
+  - [ ] 运行 `pnpm test:coverage` 确认覆盖率达标
+  - [ ] 运行 `pnpm typecheck` 确保无类型错误
+  - [ ] 运行 `pnpm test` 确保所有测试通过
+
+## Dev Notes
+
+### 当前测试覆盖度分析
+
+**已覆盖场景** (13 个测试):
+- ✅ 列表加载(有数据、空数据)
+- ✅ 创建广告/广告类型
+- ✅ 编辑广告/广告类型
+- ✅ 删除广告/广告类型
+- ✅ 搜索功能
+- ✅ 创建表单状态切换(广告类型)
+
+**未覆盖场景** (待补充):
+- ❌ API 错误处理(网络错误、服务器错误、业务错误)
+- ❌ 表单验证失败(必填字段、格式验证、长度限制)
+- ❌ 分页功能(页码切换、每页数量、边界条件)
+- ❌ 编辑表单状态切换(广告类型的编辑表单开关)
+- ❌ 广告类型选择器交互(打开、选择、取消)
+- ❌ 图片选择器交互(选择、清除、预览)
+
+### 测试文件结构
+
+**新增测试文件**:
+```
+packages/unified-advertisement-management-ui/tests/
+├── integration/
+│   ├── error-handling.integration.test.tsx    # [新增] API错误处理测试
+│   ├── pagination.integration.test.tsx        # [新增] 分页功能测试
+│   ├── form-validation.integration.test.tsx   # [新增] 表单验证测试
+│   ├── unified-advertisement-management.integration.test.tsx  # [扩展] 添加验证测试
+│   └── unified-advertisement-type-management.integration.test.tsx  # [扩展] 添加编辑状态测试
+├── components/
+│   └── UnifiedAdvertisementTypeSelector.test.tsx  # [新增] 选择器组件测试
+└── setup.ts                                    # [扩展] 添加错误mock
+```
+
+### API 错误处理测试规范
+
+**错误场景分类**:
+```typescript
+// 1. 网络错误(fetch 失败)
+describe('网络错误处理', () => {
+  it('应该显示网络错误提示当 API 调用失败时', async () => {
+    mockGetClient.mockReturnValue({
+      index: {
+        $get: vi.fn().mockRejectedValue(new TypeError('Failed to fetch')),
+        $post: vi.fn()
+      },
+      ':id': { $get: vi.fn(), $put: vi.fn(), $delete: vi.fn() }
+    } as any);
+
+    renderWithProviders(<UnifiedAdvertisementManagement />);
+
+    await waitFor(() => {
+      expect(toast.error).toHaveBeenCalledWith(
+        expect.stringContaining('网络错误')
+      );
+    });
+  });
+});
+
+// 2. 服务器错误(500, 503)
+it('应该显示服务器错误提示', async () => {
+  mockGetClient.mockReturnValue({
+    index: {
+      $get: vi.fn().mockResolvedValue({
+        json: async () => ({ code: 500, message: 'Internal Server Error' }),
+        status: 500
+      }),
+      $post: vi.fn()
+    },
+    ':id': { $get: vi.fn(), $put: vi.fn(), $delete: vi.fn() }
+  } as any);
+  // ... 验证错误消息
+});
+
+// 3. 业务错误(400, 404, 409)
+it('应该显示业务错误提示(400 验证失败)', async () => {
+  mockGetClient.mockReturnValue({
+    index: {
+      $post: vi.fn().mockResolvedValue({
+        json: async () => ({ code: 400, message: '标题不能为空' }),
+        status: 400
+      })
+    },
+    // ...
+  } as any);
+  // ... 验证错误消息
+});
+
+it('应该显示冲突错误提示(409 重复)', async () => {
+  mockGetClient.mockReturnValue({
+    index: {
+      $post: vi.fn().mockResolvedValue({
+        json: async () => ({ code: 409, message: '广告别名已存在' }),
+        status: 409
+      })
+    },
+    // ...
+  } as any);
+  // ... 验证错误消息
+});
+```
+
+### 表单验证测试规范
+
+**必填字段验证**:
+```typescript
+describe('表单验证', () => {
+  it('应该显示验证错误当提交空标题时', async () => {
+    const user = userEvent.setup();
+
+    renderWithProviders(<UnifiedAdvertisementManagement />);
+
+    await user.click(screen.getByTestId('create-unified-advertisement-button'));
+    await waitFor(() => {
+      expect(screen.getByTestId('modal-title')).toBeInTheDocument();
+    });
+
+    // 直接提交不填写任何字段
+    await user.click(screen.getByTestId('create-submit-button'));
+
+    await waitFor(() => {
+      expect(screen.getByText(/标题.*/)).toBeInTheDocument();
+    });
+  });
+
+  it('应该显示验证错误当未选择广告类型时', async () => {
+    // ... 类似测试
+  });
+
+  it('应该限制标题长度最多30个字符', async () => {
+    // ... 填写31个字符,验证错误消息
+  });
+});
+```
+
+### 分页功能测试规范
+
+```typescript
+describe('分页功能', () => {
+  it('应该正确切换到下一页', async () => {
+    const user = userEvent.setup();
+
+    // Mock 返回第二页数据
+    mockGetClient.mockReturnValue({
+      index: {
+        $get: vi.fn()
+          .mockResolvedValueOnce({ json: async () => ({ data: { list: [...], total: 25 } }), status: 200 })  // 第1页
+          .mockResolvedValueOnce({ json: async () => ({ data: { list: [...], total: 25 } }), status: 200 })  // 第2页
+        ,
+        $post: vi.fn()
+      },
+      ':id': { /* ... */ }
+    } as any);
+
+    renderWithProviders(<UnifiedAdvertisementManagement />);
+
+    // 点击下一页按钮
+    await user.click(screen.getByTestId('next-page-button'));
+
+    await waitFor(() => {
+      expect(mockGetClient().index.$get).toHaveBeenCalledWith({
+        query: expect.objectContaining({ page: 2 })
+      });
+    });
+  });
+
+  it('应该禁用上一页按钮在第一页时', async () => {
+    // ... 验证按钮禁用状态
+  });
+});
+```
+
+### 选择器组件测试规范
+
+```typescript
+describe('UnifiedAdvertisementTypeSelector', () => {
+  it('应该打开下拉菜单当点击触发器时', async () => {
+    const user = userEvent.setup();
+    const mockTypes = [{ id: 1, name: '首页轮播', code: 'home' }];
+
+    renderWithProviders(
+      <UnifiedAdvertisementTypeSelector
+        value={undefined}
+        onChange={vi.fn()}
+        options={mockTypes}
+        testId="type-selector"
+      />
+    );
+
+    await user.click(screen.getByTestId('type-selector-trigger'));
+
+    await waitFor(() => {
+      expect(screen.getByTestId('type-selector-content')).toBeVisible();
+    });
+  });
+
+  it('应该调用 onChange 当选择类型时', async () => {
+    const user = userEvent.setup();
+    const handleChange = vi.fn();
+    const mockTypes = [{ id: 1, name: '首页轮播', code: 'home' }];
+
+    renderWithProviders(
+      <UnifiedAdvertisementTypeSelector
+        value={undefined}
+        onChange={handleChange}
+        options={mockTypes}
+        testId="type-selector"
+      />
+    );
+
+    await user.click(screen.getByTestId('type-selector-trigger'));
+    await user.click(screen.getByTestId('type-selector-item-1'));
+
+    expect(handleChange).toHaveBeenCalledWith(1);
+  });
+});
+```
+
+### vitest.config.ts 覆盖率配置
+
+```typescript
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    setupFiles: ['./tests/setup.ts'],
+    include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+    fileParallelism: false,
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'node_modules/',
+        'tests/',
+        '**/*.test.{ts,tsx}',
+        '**/*.spec.{ts,tsx}',
+        'src/types/index.ts',  // 类型文件不计入覆盖率
+      ],
+      thresholds: {
+        statements: 70,
+        branches: 65,
+        functions: 70,
+        lines: 70
+      }
+    }
+  }
+});
+```
+
+### package.json 版本修复
+
+**问题**: vitest 和 @vitest/coverage-v8 版本不匹配
+
+**解决方案**:
+```json
+{
+  "devDependencies": {
+    "vitest": "^4.0.10",
+    "@vitest/coverage-v8": "^4.0.10"  // 版本号与 vitest 一致
+  }
+}
+```
+
+### 参考文档
+
+- [UI包开发规范](../architecture/ui-package-standards.md)
+- [Web UI包测试规范](../architecture/web-ui-testing-standards.md)
+- [测试策略概述](../architecture/testing-strategy.md)
+
+## Testing
+
+### 测试文件位置
+- 错误处理测试: `tests/integration/error-handling.integration.test.tsx`
+- 分页功能测试: `tests/integration/pagination.integration.test.tsx`
+- 表单验证测试: `tests/integration/form-validation.integration.test.tsx`
+- 选择器组件测试: `tests/components/UnifiedAdvertisementTypeSelector.test.tsx`
+
+### 测试框架
+- **Vitest**: 主要测试运行器
+- **Testing Library**: React组件测试
+- **userEvent**: 用户交互模拟
+
+### 测试标准
+
+| 测试类型 | 当前覆盖 | 目标覆盖 |
+|----------|----------|----------|
+| 组件测试 | ~60% | 70%+ |
+| 集成测试 | ~70% | 80%+ |
+
+### 测试执行命令
+```bash
+cd packages/unified-advertisement-management-ui
+
+# 运行所有测试
+pnpm test
+
+# 生成覆盖率报告
+pnpm test:coverage
+
+# 运行特定测试文件
+pnpm test error-handling.integration.test.tsx
+```
+
+## Change Log
+
+| Date | Version | Description | Author |
+|------|---------|-------------|--------|
+| 2026-01-03 | 1.0 | 初始故事创建 | James (Claude Code) |
+
+## Dev Agent Record
+
+### Agent Model Used
+_待开发代理填写_
+
+### Debug Log References
+_待开发代理填写_
+
+### Completion Notes List
+_待开发代理填写_
+
+### File List
+_待开发代理填写_
+
+## QA Results
+_QA代理待填写_