Prechádzať zdrojové kódy

docs(e2e-test-utils): 创建 Story 3.3 - 在 E2E 测试中验证文件上传工具

- 创建独立的文件上传 E2E 测试文件
- 测试场景包括基本上传、多文件上传、完整表单提交
- 使用真实 fixtures 文件和 testId 选择器
- 应用 Epic 2 关键经验:真实 E2E 测试不可替代
- 更新 sprint-status: Story 3-3 状态为 ready-for-dev

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 1 týždeň pred
rodič
commit
35c20e813a

+ 424 - 0
_bmad-output/implementation-artifacts/3-3-upload-e2e-integration.md

@@ -0,0 +1,424 @@
+# Story 3.3: 在 E2E 测试中验证文件上传工具
+
+Status: ready-for-dev
+
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
+
+## Story
+
+作为 E2E 测试开发者,
+我想要在真实的 E2E 测试中使用 `uploadFileToField()` 工具函数,
+以便验证文件上传工具在实际场景中的可用性和稳定性。
+
+## Acceptance Criteria
+
+**Given** Story 3.1 已实现 `uploadFileToField()` 函数(含 UI 组件 testId 支持)
+**Given** Story 3.2 已完成单元测试(覆盖率 91.66%)
+**Given** PhotoUploadField 组件已添加 testId prop(`testId={"photo-upload-${index}"}`)
+
+**When** 在 `web/tests/e2e/` 中创建或更新测试文件
+
+**Then** 验收标准如下:
+
+1. **创建独立的文件上传 E2E 测试文件**
+   - 创建 `web/tests/e2e/specs/admin/file-upload-validation.spec.ts`
+   - 测试文件验证 `uploadFileToField()` 在真实场景中的可用性
+   - 测试使用真实 MinIO 文件上传流程(非模拟)
+
+2. **测试场景包括**
+   - 基本文件上传:使用 testId 选择器上传照片
+   - 多文件上传:连续上传多张照片
+   - 不同文件类型:上传不同格式的图片文件
+   - 错误处理:文件不存在、选择器无效等场景
+
+3. **使用真实 fixtures 文件**
+   - 使用 `web/tests/fixtures/images/` 中的真实图片文件
+   - 验证文件路径解析正确(相对于 web/tests/fixtures)
+   - 验证文件上传后能正确显示预览
+
+4. **测试在真实浏览器中运行**
+   - 使用 Playwright 运行完整测试
+   - 测试通过率 100%
+   - 无超时或失败
+
+5. **收集反馈并记录问题**
+   - 记录测试过程中发现的任何问题
+   - 验证工具的易用性和错误提示
+   - 确认单元测试未覆盖的问题
+
+## Tasks / Subtasks
+
+- [ ] **Task 1: 创建文件上传 E2E 测试文件** (AC: #1, #2)
+  - [ ] Subtask 1.1: 创建 `file-upload-validation.spec.ts` 测试文件
+  - [ ] Subtask 1.2: 导入 `uploadFileToField` 函数
+  - [ ] Subtask 1.3: 创建测试套件和 fixture 设置
+
+- [ ] **Task 2: 实现基本文件上传测试** (AC: #2, #3)
+  - [ ] Subtask 2.1: 测试单张照片上传(身份证照片)
+  - [ ] Subtask 2.2: 使用 testId 选择器 `[data-testid="photo-upload-0"]`
+  - [ ] Subtask 2.3: 验证上传成功后显示预览
+
+- [ ] **Task 3: 实现多文件上传测试** (AC: #2)
+  - [ ] Subtask 3.1: 测试连续上传多张照片
+  - [ ] Subtask 3.2: 使用不同的 testId (`photo-upload-1`, `photo-upload-2`)
+  - [ ] Subtask 3.3: 验证每张照片上传成功
+
+- [ ] **Task 4: 实现 E2E 集成测试(完整表单场景)** (AC: #2, #4)
+  - [ ] Subtask 4.1: 测试完整的残疾人创建流程(含照片上传)
+  - [ ] Subtask 4.2: 验证表单提交成功
+  - [ ] Subtask 4.3: 验证数据保存正确
+
+- [ ] **Task 5: 运行测试并收集结果** (AC: #4, #5)
+  - [ ] Subtask 5.1: 运行 E2E 测试:`pnpm test:e2e:chromium file-upload-validation`
+  - [ ] Subtask 5.2: 记录测试结果和任何错误
+  - [ ] Subtask 5.3: 保存失败截图(如果有)
+
+- [ ] **Task 6: 更新 disability-person.page.ts** (可选 - AC: #4)
+  - [ ] Subtask 6.1: 更新 `uploadPhoto()` 方法使用 `uploadFileToField()`
+  - [ ] Subtask 6.2: 简化 DOM 操作,使用 testId 选择器
+  - [ ] Subtask 6.3: 删除复杂的 XPath 查询和模拟文件逻辑
+
+- [ ] **Task 7: 编写测试报告** (AC: #5)
+  - [ ] Subtask 7.1: 记录测试覆盖率(哪些场景已验证)
+  - [ ] Subtask 7.2: 记录发现的问题和修复建议
+  - [ ] Subtask 7.3: 确认单元测试未覆盖的场景
+
+## Dev Notes
+
+### Epic 3 背景与目标
+
+**Epic 3: 文件上传工具开发与验证**
+
+遵循 Epic 2 的成功模式,开发文件上传工具并在真实 E2E 测试中验证,解决当前测试超时阻塞问题。
+
+**模式:** 工具开发 → 真实 E2E 测试验证 → 问题修复 → 稳定性验证
+
+**当前进度:**
+- Story 3.1: ✅ 已完成 - `uploadFileToField()` 函数已实现(含 UI 组件 testId 支持)
+- Story 3.2: ✅ 已完成 - 单元测试(覆盖率 91.66%)
+- Story 3.3: 🔄 本 Story - 在真实 E2E 测试中验证文件上传工具
+
+### ⚠️ Epic 2 关键经验应用(必须阅读)
+
+**单元测试的局限性(来自 Epic 2 回顾):**
+
+1. **单元测试无法发现真实 DOM 问题**
+   - Epic 2 的 Select 工具单元测试覆盖率 93.65%,仍然无法发现 DOM 结构问题
+   - 原因:单元测试使用模拟 DOM,不是真实的 Radix UI 组件
+   - **教训:** 必须在真实 E2E 环境中验证工具
+
+2. **真实 E2E 测试不可替代**
+   - DOM 结构假设必须基于真实组件验证
+   - 选择器策略必须在真实浏览器中测试
+   - 集成测试是必需的,不是可选项
+
+3. **本 Story 的定位**
+   - 单元测试(Story 3.2)验证了基本逻辑和错误处理
+   - **本 Story** 验证与真实 DOM 的交互和完整文件上传流程
+   - 重点测试:真实文件上传、浏览器行为、MinIO 集成
+
+### 技术规范
+
+#### 当前 E2E 测试文件上传实现分析
+
+**disability-person.page.ts 中的 uploadPhoto() 方法(第 176-205 行):**
+
+```typescript
+async uploadPhoto(photoType: string, fileName: string) {
+  // ❌ 问题 1: 使用复杂的 XPath 查找
+  const photoSection = this.page.locator('text=' + photoType).first();
+  await photoSection.scrollIntoViewIfNeeded();
+
+  const uploadButton = photoSection.locator('xpath=ancestor::div[contains(@class, "space-y-")]').first()
+    .getByRole('button', { name: /上传/ }).first();
+
+  // ❌ 问题 2: 使用 evaluateHandle 查找隐藏的 file input
+  const fileInput = await uploadButton.evaluateHandle((el: any) => {
+    const input = el.querySelector('input[type="file"]');
+    return input;
+  });
+
+  // ❌ 问题 3: 创建假图片文件
+  const file = {
+    name: fileName,
+    mimeType: 'image/jpeg',
+    buffer: Buffer.from('fake image content')
+  };
+
+  await fileInput.uploadFile(file as any);
+}
+```
+
+**问题总结:**
+1. XPath 查询脆弱且难以维护
+2. `evaluateHandle` 增加复杂度
+3. 假文件内容无法测试真实图片上传流程
+
+#### 新的 uploadFileToField() 工具函数
+
+**函数签名(已实现):**
+```typescript
+import { uploadFileToField } from '@d8d/e2e-test-utils';
+
+await uploadFileToField(
+  page,                          // Playwright Page 对象
+  '[data-testid="photo-upload-0"]', // 选择器
+  'sample-id-card.jpg',          // 文件名(相对于 web/tests/fixtures)
+  { timeout: 5000 }              // 可选配置
+);
+```
+
+**关键特性:**
+1. ✅ 使用 `page.locator().setInputFiles()` API(Playwright 标准)
+2. ✅ 自动解析 fixtures 路径(默认 `web/tests/fixtures`)
+3. ✅ 完整的错误处理(文件不存在、选择器无效)
+4. ✅ 支持真实文件上传
+
+#### PhotoUploadField testId 架构
+
+**组件结构(PhotoUploadField.tsx 第 172 行):**
+```tsx
+<FileSelector
+  value={photo.fileId}
+  onChange={(fileId) => handleFileIdChange(index, fileId)}
+  testId={`photo-upload-${index}`}  // ← 关键:生成唯一 testId
+/>
+```
+
+**FileSelector → MinioUploader 传递链:**
+```
+PhotoUploadField (testId={"photo-upload-${index}"})
+    ↓
+FileSelector (testId prop)
+    ↓
+MinioUploader (添加隐藏的 <input type="file" data-testid={testId}>)
+```
+
+**测试使用方式:**
+```typescript
+// 上传第 1 张照片(身份证照片)
+await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
+
+// 上传第 2 张照片(残疾证照片)
+await uploadFileToField(page, '[data-testid="photo-upload-1"]', 'sample-disability-card.jpg');
+```
+
+### 测试场景设计
+
+#### 场景 1: 基本文件上传验证
+
+**测试目标:** 验证 `uploadFileToField()` 能成功上传单个文件
+
+```typescript
+test('应该成功上传单张照片', async ({ disabilityPersonPage, page }) => {
+  const timestamp = Date.now();
+
+  // 1. 打开对话框并填写基本信息
+  await disabilityPersonPage.openCreateDialog();
+  await disabilityPersonPage.fillBasicForm({
+    name: `文件上传测试_${timestamp}`,
+    gender: '男',
+    idCard: '420101199001011234',
+    disabilityId: '51100119900101',
+    disabilityType: '视力残疾',
+    disabilityLevel: '一级',
+    phone: '13800138000',
+    idAddress: '湖北省武汉市测试街道1号',
+    province: '湖北省',
+    city: '武汉市'
+  });
+
+  // 2. 滚动到照片区域并添加一张照片
+  await disabilityPersonPage.scrollToSection('照片');
+  await page.getByTestId('add-photo-button').click();
+
+  // 3. 使用 uploadFileToField 上传文件
+  await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
+
+  // 4. 验证上传成功(检查预览或文件名显示)
+  const preview = page.locator('[data-testid="photo-upload-0"]').locator('img');
+  await expect(preview).toBeVisible({ timeout: 5000 });
+});
+```
+
+#### 场景 2: 多文件上传验证
+
+**测试目标:** 验证连续上传多张文件
+
+```typescript
+test('应该成功上传多张照片', async ({ disabilityPersonPage, page }) => {
+  const timestamp = Date.now();
+
+  // 1. 打开对话框并填写基本信息
+  await disabilityPersonPage.openCreateDialog();
+  await disabilityPersonPage.fillBasicForm({...});
+
+  // 2. 滚动到照片区域
+  await disabilityPersonPage.scrollToSection('照片');
+
+  // 3. 添加三张照片
+  await page.getByTestId('add-photo-button').click();
+  await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
+
+  await page.getByTestId('add-photo-button').click();
+  await uploadFileToField(page, '[data-testid="photo-upload-1"]', 'sample-disability-card.jpg');
+
+  await page.getByTestId('add-photo-button').click();
+  await uploadFileToField(page, '[data-testid="photo-upload-2"]', 'sample-id-card.jpg');
+
+  // 4. 验证所有照片都上传成功
+  const previews = page.locator('[data-testid^="photo-upload-"]').locator('img');
+  await expect(previews).toHaveCount(3);
+});
+```
+
+#### 场景 3: 完整表单提交验证
+
+**测试目标:** 验证文件上传后的表单能成功提交
+
+```typescript
+test('应该成功提交包含照片的表单', async ({ disabilityPersonPage, page }) => {
+  const timestamp = Date.now();
+
+  // 1. 打开对话框并填写基本信息
+  await disabilityPersonPage.openCreateDialog();
+  await disabilityPersonPage.fillBasicForm({...});
+
+  // 2. 上传照片
+  await disabilityPersonPage.scrollToSection('照片');
+  await page.getByTestId('add-photo-button').click();
+  await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
+
+  // 3. 提交表单
+  const result = await disabilityPersonPage.submitForm();
+
+  // 4. 验证提交成功
+  expect(result.hasSuccess).toBe(true);
+  expect(result.hasError).toBe(false);
+
+  // 5. 验证数据保存
+  await disabilityPersonPage.waitForDialogClosed();
+  await page.reload();
+  await disabilityPersonPage.goto();
+  await disabilityPersonPage.searchByName(`文件上传测试_${timestamp}`);
+
+  const personExists = await disabilityPersonPage.personExists(`文件上传测试_${timestamp}`);
+  expect(personExists).toBe(true);
+});
+```
+
+### 项目结构说明
+
+**测试文件位置:**
+```
+web/tests/
+├── e2e/
+│   ├── specs/
+│   │   └── admin/
+│   │       ├── file-upload-validation.spec.ts  # 本 Story 创建
+│   │       └── disability-person-complete.spec.ts  # 现有测试
+│   ├── fixtures/
+│   │   └── images/
+│   │       ├── sample-id-card.jpg           # 已存在
+│   │       └── sample-disability-card.jpg   # 已存在
+│   └── pages/
+│       └── admin/
+│           └── disability-person.page.ts    # 现有 Page Object
+```
+
+**包依赖:**
+```typescript
+// web/tests/e2e/specs/admin/file-upload-validation.spec.ts
+import { uploadFileToField } from '@d8d/e2e-test-utils';
+```
+
+### 运行测试命令
+
+**运行本 Story 的测试:**
+```bash
+# 从项目根目录
+cd web && pnpm test:e2e:chromium file-upload-validation
+
+# 快速失败模式(推荐调试时使用)
+timeout 60 pnpm test:e2e:chromium file-upload-validation
+```
+
+**运行完整的残疾人管理测试(验证集成):**
+```bash
+cd web && pnpm test:e2e:chromium disability-person-complete
+```
+
+### 参考文档
+
+**架构文档:**
+- `_bmad-output/planning-artifacts/architecture.md` - E2E 测试标准、三层测试策略
+
+**E2E 测试标准:**
+- `docs/standards/e2e-radix-testing.md` - 文件上传测试标准
+
+**Epic 3 完整需求:**
+- 相关故事文件已在 `_bmad-output/implementation-artifacts/` 中
+
+**Epic 2 回顾(关键经验):**
+- `_bmad-output/implementation-artifacts/epic-2-retrospective.md` - 真实 E2E 测试的价值
+
+**TypeScript + Playwright 陷阱:**
+- `architecture.md` 第 533-657 行 - DOM 结构假设必须验证
+
+**已实现的源文件参考:**
+- [Source: packages/e2e-test-utils/src/file-upload.ts] - uploadFileToField 函数实现
+- [Source: allin-packages/disability-person-management-ui/src/components/PhotoUploadField.tsx] - testId 架构
+- [Source: web/tests/e2e/pages/admin/disability-person.page.ts] - 当前 uploadPhoto 方法
+
+### Project Structure Notes
+
+**Monorepo 结构对齐:**
+- E2E 测试在 `web/tests/e2e/` 目录
+- 使用 `@d8d/e2e-test-utils` workspace 包
+- 测试 fixtures 位于 `web/tests/fixtures/`
+
+**文件组织:**
+- 测试文件按功能组织在 `specs/admin/` 目录
+- 使用 `.spec.ts` 后缀命名(Playwright 约定)
+- Page Objects 位于 `pages/admin/` 目录
+
+### References
+
+**源文档引用:**
+- [Source: _bmad-output/implementation-artifacts/3-1-file-upload-tool.md] - 文件上传工具实现
+- [Source: _bmad-output/implementation-artifacts/3-2-upload-unit-tests.md] - 单元测试结果
+- [Source: _bmad-output/implementation-artifacts/epic-2-retrospective.md] - E2E 验证经验
+
+**现有测试参考:**
+- [Source: web/tests/e2e/specs/admin/disability-person-complete.spec.ts] - 完整流程测试
+- [Source: web/tests/e2e/pages/admin/disability-person.page.ts] - Page Object 实现
+
+## Dev Agent Record
+
+### Agent Model Used
+
+Claude Opus 4 (claude-opus-4-5-20251101)
+
+### Debug Log References
+
+### Completion Notes List
+
+_本 Story 正在开发中,完成时将记录:_
+- 创建的测试文件和测试用例数量
+- 测试执行结果(通过/失败)
+- 发现的问题和修复记录
+
+### File List
+
+_本 Story 预计创建/修改的文件:_
+
+**新增文件:**
+- `web/tests/e2e/specs/admin/file-upload-validation.spec.ts` - 文件上传 E2E 验证测试
+
+**可能修改的文件:**
+- `web/tests/e2e/pages/admin/disability-person.page.ts` - 更新 uploadPhoto() 方法使用 uploadFileToField()
+
+---
+
+**Story 创建日期:** 2026-01-10
+**Story 状态:** ready-for-dev

+ 1 - 1
_bmad-output/implementation-artifacts/sprint-status.yaml

@@ -66,7 +66,7 @@ development_status:
   epic-3: in-progress
   3-1-file-upload-tool: done             # 开发文件上传工具函数(含 UI 组件架构改进)
   3-2-upload-unit-tests: done             # 编写文件上传工具的单元测试
-  3-3-upload-e2e-integration: backlog    # 在 web/tests/e2e 中验证文件上传工具
+  3-3-upload-e2e-integration: ready-for-dev    # 在 web/tests/e2e 中验证文件上传工具
   3-4-collect-feedback-fix: backlog      # 收集反馈并修复问题
   3-5-upload-stability-test: backlog     # 文件上传稳定性验证 (10次连续运行)
   epic-3-retrospective: optional