|
@@ -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代理待填写_
|