|
|
@@ -0,0 +1,344 @@
|
|
|
+# Story 3.5: 支持多文件同时上传
|
|
|
+
|
|
|
+Status: ready-for-dev
|
|
|
+
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
+
|
|
|
+## Story
|
|
|
+
|
|
|
+作为测试开发者,
|
|
|
+我想要 `uploadFileToField()` 函数支持一次上传多个文件,
|
|
|
+以便测试前端 `<input type="file" multiple>` 的多文件选择功能。
|
|
|
+
|
|
|
+## Acceptance Criteria
|
|
|
+
|
|
|
+**Given** Story 3.1 已实现 `uploadFileToField()` 函数(单文件上传)
|
|
|
+**Given** Story 3.4 已完成,Select 工具问题已修复,测试可以顺利进行到文件上传步骤
|
|
|
+
|
|
|
+**When** 扩展 `uploadFileToField()` 函数支持多文件上传
|
|
|
+
|
|
|
+**Then** 验收标准如下:
|
|
|
+
|
|
|
+1. **函数接受文件名数组或字符串(向后兼容)**
|
|
|
+ - 单文件调用:`uploadFileToField(page, selector, 'file.jpg')` 继续工作
|
|
|
+ - 多文件调用:`uploadFileToField(page, selector, ['file1.jpg', 'file2.jpg', 'file3.jpg'])` 正常工作
|
|
|
+
|
|
|
+2. **使用 Playwright 的 `setInputFiles([path1, path2, ...])` API**
|
|
|
+ - 多文件时调用 `setInputFiles(filePathArray)`
|
|
|
+ - 单文件时调用 `setInputFiles(filePath)`(保持原有行为)
|
|
|
+
|
|
|
+3. **支持相对路径数组(相对于 fixtures 目录)**
|
|
|
+ - 每个文件路径都从 fixtures 目录解析
|
|
|
+ - 保持与单文件上传相同的路径解析逻辑
|
|
|
+
|
|
|
+4. **错误时提供清晰消息(包含所有文件路径)**
|
|
|
+ - 文件不存在时,列出所有缺失的文件
|
|
|
+ - 选择器无效时,与单文件上传行为一致
|
|
|
+
|
|
|
+5. **单元测试覆盖所有场景**
|
|
|
+ - 单文件上传(向后兼容测试)
|
|
|
+ - 多文件上传(2-3 个文件)
|
|
|
+ - 文件不存在错误(包含路径列表)
|
|
|
+ - 空数组错误处理
|
|
|
+
|
|
|
+6. **E2E 测试验证多文件上传**
|
|
|
+ - 在 `file-upload-validation.spec.ts` 中添加多文件上传场景
|
|
|
+ - 验证一次上传 3 张照片(身份证、残疾证正反面)
|
|
|
+
|
|
|
+## Tasks / Subtasks
|
|
|
+
|
|
|
+- [ ] **Task 1: 分析当前 uploadFileToField 实现** (AC: #1, #2)
|
|
|
+ - [ ] Subtask 1.1: 阅读 `packages/e2e-test-utils/src/file-upload.ts`
|
|
|
+ - [ ] Subtask 1.2: 理解当前单文件上传逻辑
|
|
|
+ - [ ] Subtask 1.3: 确定需要修改的部分
|
|
|
+
|
|
|
+- [ ] **Task 2: 扩展函数签名支持多文件** (AC: #1)
|
|
|
+ - [ ] Subtask 2.1: 使用函数重载或联合类型支持两种签名
|
|
|
+ - [ ] Subtask 2.2: 更新类型定义(`FileUploadOptions` 接口)
|
|
|
+ - [ ] Subtask 2.3: 更新 JSDoc 注释
|
|
|
+
|
|
|
+- [ ] **Task 3: 实现多文件上传逻辑** (AC: #2, #3)
|
|
|
+ - [ ] Subtask 3.1: 添加文件路径数组处理逻辑
|
|
|
+ - [ ] Subtask 3.2: 调用 `setInputFiles(filePathArray)` 处理多文件
|
|
|
+ - [ ] Subtask 3.3: 保持所有文件路径验证逻辑
|
|
|
+
|
|
|
+- [ ] **Task 4: 更新错误消息** (AC: #4)
|
|
|
+ - [ ] Subtask 4.1: 文件不存在时列出所有缺失文件
|
|
|
+ - [ ] Subtask 4.2: 空数组错误处理
|
|
|
+ - [ ] Subtask 4.3: 保持与单文件上传一致的错误格式
|
|
|
+
|
|
|
+- [ ] **Task 5: 编写单元测试** (AC: #5)
|
|
|
+ - [ ] Subtask 5.1: 单文件上传(向后兼容)
|
|
|
+ - [ ] Subtask 5.2: 多文件上传(2-3 个文件)
|
|
|
+ - [ ] Subtask 5.3: 文件不存在错误(包含路径列表)
|
|
|
+ - [ ] Subtask 5.4: 空数组错误处理
|
|
|
+
|
|
|
+- [ ] **Task 6: 在 E2E 测试中验证** (AC: #6)
|
|
|
+ - [ ] Subtask 6.1: 在 `file-upload-validation.spec.ts` 添加多文件上传场景
|
|
|
+ - [ ] Subtask 6.2: 验证一次上传 3 张照片
|
|
|
+ - [ ] Subtask 6.3: 运行测试确认通过
|
|
|
+
|
|
|
+## Dev Notes
|
|
|
+
|
|
|
+### Epic 3 背景与目标
|
|
|
+
|
|
|
+**Epic 3: 文件上传工具开发与验证**
|
|
|
+
|
|
|
+遵循 Epic 2 的成功模式,开发文件上传工具并在真实 E2E 测试中验证,解决当前测试超时阻塞问题。
|
|
|
+
|
|
|
+**模式:** 工具开发 → 真实 E2E 测试验证 → 问题修复 → 稳定性验证
|
|
|
+
|
|
|
+**当前进度:**
|
|
|
+- Story 3.1: ✅ 已完成 - `uploadFileToField()` 函数已实现(单文件上传)
|
|
|
+- Story 3.2: ✅ 已完成 - 单元测试(覆盖率 91.66%,36/36 通过)
|
|
|
+- Story 3.3: ✅ 已完成 - 在真实 E2E 测试中验证(场景 1 通过)
|
|
|
+- Story 3.4: ✅ 已完成 - 收集反馈并修复问题(修复了 Select 工具处理带 `*` 标签的问题)
|
|
|
+- Story 3.5: 🔄 本 Story - 支持多文件同时上传
|
|
|
+
|
|
|
+### 从 Story 3.4 获得的关键经验
|
|
|
+
|
|
|
+**Select 工具修复经验:**
|
|
|
+- 使用 `getByText(label, { exact: true })` 代替 `text=` 选择器更稳定
|
|
|
+- 处理特殊字符(如 `*`)需要额外的策略
|
|
|
+
|
|
|
+**文件上传工具验证结果:**
|
|
|
+- ✅ 单元测试 36/36 通过
|
|
|
+- ✅ fixtures 路径解析正确
|
|
|
+- ✅ setInputFiles API 调用正确
|
|
|
+- ✅ testId 选择器支持正常
|
|
|
+- ✅ 错误处理机制完整
|
|
|
+
|
|
|
+### 前端组件支持多文件上传
|
|
|
+
|
|
|
+**MinioUploader 组件:**
|
|
|
+```tsx
|
|
|
+// packages/file-management-ui/src/components/MinioUploader.tsx
|
|
|
+<input
|
|
|
+ type="file"
|
|
|
+ multiple={multiple} // 已支持 multiple 属性
|
|
|
+ className="hidden"
|
|
|
+/>
|
|
|
+```
|
|
|
+
|
|
|
+前端组件已经支持多文件选择,但 E2E 测试工具 `uploadFileToField()` 只支持单文件上传。
|
|
|
+
|
|
|
+### 技术实现要点
|
|
|
+
|
|
|
+#### 函数签名设计
|
|
|
+
|
|
|
+使用 TypeScript 函数重载支持两种调用方式:
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 单文件上传(向后兼容)
|
|
|
+export async function uploadFileToField(
|
|
|
+ page: Page,
|
|
|
+ selector: string,
|
|
|
+ fileName: string,
|
|
|
+ options?: FileUploadOptions
|
|
|
+): Promise<void>
|
|
|
+
|
|
|
+// 多文件上传
|
|
|
+export async function uploadFileToField(
|
|
|
+ page: Page,
|
|
|
+ selector: string,
|
|
|
+ fileNames: string[],
|
|
|
+ options?: FileUploadOptions
|
|
|
+): Promise<void>
|
|
|
+```
|
|
|
+
|
|
|
+#### Playwright API
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 单文件上传
|
|
|
+await page.locator(selector).setInputFiles(filePath)
|
|
|
+
|
|
|
+// 多文件上传
|
|
|
+await page.locator(selector).setInputFiles([path1, path2, path3])
|
|
|
+```
|
|
|
+
|
|
|
+#### 路径解析逻辑
|
|
|
+
|
|
|
+保持与单文件上传相同的逻辑:
|
|
|
+- 相对路径相对于 `fixturesDir`(默认为 `tests/fixtures`)
|
|
|
+- 绝对路径直接使用
|
|
|
+- 验证所有文件存在后再上传
|
|
|
+
|
|
|
+#### 错误处理
|
|
|
+
|
|
|
+```typescript
|
|
|
+// 文件不存在错误(多文件)
|
|
|
+Error: 文件上传失败:部分文件不存在
|
|
|
+ 选择器: [data-testid="photo-upload-0"]
|
|
|
+ 缺失文件:
|
|
|
+ - images/not-exist-1.jpg
|
|
|
+ - images/not-exist-2.jpg
|
|
|
+ 可用文件:
|
|
|
+ - images/sample-id-card.jpg
|
|
|
+ - images/sample-disability-card.jpg
|
|
|
+
|
|
|
+// 空数组错误
|
|
|
+Error: 文件上传失败:文件列表为空
|
|
|
+ 选择器: [data-testid="photo-upload-0"]
|
|
|
+ 提示: 至少需要一个文件路径
|
|
|
+```
|
|
|
+
|
|
|
+### 测试场景
|
|
|
+
|
|
|
+#### 单元测试场景
|
|
|
+
|
|
|
+```typescript
|
|
|
+describe('uploadFileToField - 多文件上传', () => {
|
|
|
+ test('应该成功上传多个文件', async () => {
|
|
|
+ await uploadFileToField(page, selector, [
|
|
|
+ 'images/sample-id-card.jpg',
|
|
|
+ 'images/sample-disability-card.jpg',
|
|
|
+ 'images/sample-photo.jpg'
|
|
|
+ ])
|
|
|
+ // 验证 setInputFiles 被调用一次,参数为文件路径数组
|
|
|
+ })
|
|
|
+
|
|
|
+ test('应该保持向后兼容(单文件)', async () => {
|
|
|
+ await uploadFileToField(page, selector, 'images/sample-id-card.jpg')
|
|
|
+ // 验证单文件上传仍然工作
|
|
|
+ })
|
|
|
+
|
|
|
+ test('应该在文件不存在时抛出错误', async () => {
|
|
|
+ await expect(
|
|
|
+ uploadFileToField(page, selector, ['not-exist.jpg'])
|
|
|
+ ).rejects.toThrow('文件不存在')
|
|
|
+ })
|
|
|
+
|
|
|
+ test('应该在空数组时抛出错误', async () => {
|
|
|
+ await expect(
|
|
|
+ uploadFileToField(page, selector, [])
|
|
|
+ ).rejects.toThrow('文件列表为空')
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+#### E2E 测试场景
|
|
|
+
|
|
|
+```typescript
|
|
|
+test('场景 2:应该成功上传多张照片(一次性)', async () => {
|
|
|
+ await page.goto('/admin/disability-person/create')
|
|
|
+
|
|
|
+ // 填写基本信息(使用已修复的 Select 工具)
|
|
|
+ await selectRadixOption(page, '性别', '男')
|
|
|
+
|
|
|
+ // 一次性上传 3 张照片
|
|
|
+ await uploadFileToField(page, '[data-testid="photo-upload-0"]', [
|
|
|
+ 'images/sample-id-card.jpg',
|
|
|
+ 'images/sample-disability-card.jpg',
|
|
|
+ 'images/sample-photo.jpg'
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 验证上传成功
|
|
|
+ await expect(page.locator('.photo-preview')).toHaveCount(3)
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+### 项目结构说明
|
|
|
+
|
|
|
+**相关文件:**
|
|
|
+```
|
|
|
+packages/e2e-test-utils/
|
|
|
+├── src/
|
|
|
+│ └── file-upload.ts # 需要修改:添加多文件支持
|
|
|
+├── tests/
|
|
|
+│ └── unit/
|
|
|
+│ └── file-upload.test.ts # 需要修改:添加多文件测试
|
|
|
+
|
|
|
+web/tests/e2e/
|
|
|
+├── specs/admin/
|
|
|
+│ └── file-upload-validation.spec.ts # 需要修改:添加多文件场景
|
|
|
+├── fixtures/images/
|
|
|
+│ ├── sample-id-card.jpg
|
|
|
+│ ├── sample-disability-card-front.jpg
|
|
|
+│ └── sample-disability-card-back.jpg
|
|
|
+└── pages/admin/
|
|
|
+ └── disability-person.page.ts # 可能需要修改:使用多文件上传
|
|
|
+```
|
|
|
+
|
|
|
+### 参考文档
|
|
|
+
|
|
|
+**架构文档:**
|
|
|
+- `_bmad-output/planning-artifacts/architecture.md` - 类型系统、错误处理策略
|
|
|
+
|
|
|
+**E2E 测试标准:**
|
|
|
+- `docs/standards/e2e-radix-testing.md` - 文件上传测试标准
|
|
|
+
|
|
|
+**Epic 3 完整需求:**
|
|
|
+- `_bmad-output/planning-artifacts/epics.md` - Epic 3 和 Story 3.5 详细需求
|
|
|
+
|
|
|
+**前置 Story 文件:**
|
|
|
+- `_bmad-output/implementation-artifacts/3-1-file-upload-tool.md` - 单文件上传工具实现
|
|
|
+- `_bmad-output/implementation-artifacts/3-2-upload-unit-tests.md` - 单元测试
|
|
|
+- `_bmad-output/implementation-artifacts/3-3-upload-e2e-integration.md` - E2E 集成测试
|
|
|
+- `_bmad-output/implementation-artifacts/3-4-collect-feedback-fix.md` - 收集反馈并修复问题
|
|
|
+
|
|
|
+**Epic 2 经验(关键):**
|
|
|
+- `_bmad-output/implementation-artifacts/epic-2-retrospective.md` - DOM 结构假设必须验证
|
|
|
+
|
|
|
+### Playwright setInputFiles API 参考
|
|
|
+
|
|
|
+```typescript
|
|
|
+// Playwright Locator API
|
|
|
+class Locator {
|
|
|
+ // 设置文件输入框的值
|
|
|
+ setInputFiles(files: Path | string | Buffer | Array<Path | string | Buffer | FilePayload>): Promise<void>
|
|
|
+}
|
|
|
+
|
|
|
+// 使用示例
|
|
|
+await page.locator('input[type="file"]').setInputFiles('path/to/file.jpg')
|
|
|
+await page.locator('input[type="file"]').setInputFiles([
|
|
|
+ 'path/to/file1.jpg',
|
|
|
+ 'path/to/file2.jpg',
|
|
|
+ 'path/to/file3.jpg'
|
|
|
+])
|
|
|
+```
|
|
|
+
|
|
|
+### Project Structure Notes
|
|
|
+
|
|
|
+**Monorepo 结构对齐:**
|
|
|
+- E2E 测试工具在 `packages/e2e-test-utils/` 目录
|
|
|
+- 测试在 `web/tests/e2e/` 目录
|
|
|
+- 使用 `@d8d/e2e-test-utils` workspace 包
|
|
|
+
|
|
|
+**文件组织:**
|
|
|
+- 工具函数按功能分组(file-upload.ts, radix-select.ts)
|
|
|
+- 测试文件与源文件对应(unit/file-upload.test.ts)
|
|
|
+- E2E 测试按功能模块组织
|
|
|
+
|
|
|
+### References
|
|
|
+
|
|
|
+**源文档引用:**
|
|
|
+- [Source: _bmad-output/planning-artifacts/epics.md#Epic-3-Story-3.5]
|
|
|
+- [Source: _bmad-output/implementation-artifacts/3-1-file-upload-tool.md] - 单文件上传实现
|
|
|
+- [Source: packages/file-management-ui/src/components/MinioUploader.tsx] - 前端多文件支持
|
|
|
+
|
|
|
+**Playwright 文档:**
|
|
|
+- [https://playwright.dev/docs/api/class-locator#locator-set-input-files](https://playwright.dev/docs/api/class-locator#locator-set-input-files)
|
|
|
+
|
|
|
+## Dev Agent Record
|
|
|
+
|
|
|
+### Agent Model Used
|
|
|
+
|
|
|
+Claude Opus 4 (claude-opus-4-5-20251101)
|
|
|
+
|
|
|
+### Debug Log References
|
|
|
+
|
|
|
+### Completion Notes List
|
|
|
+
|
|
|
+_本 Story 创建于 2026-01-10_
|
|
|
+
|
|
|
+### File List
|
|
|
+
|
|
|
+_待创建/修改的文件:_
|
|
|
+- `packages/e2e-test-utils/src/file-upload.ts` - 添加多文件上传支持
|
|
|
+- `packages/e2e-test-utils/tests/unit/file-upload.test.ts` - 添加多文件测试
|
|
|
+- `web/tests/e2e/specs/admin/file-upload-validation.spec.ts` - 添加多文件 E2E 场景
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+**Story 创建日期:** 2026-01-10
|
|
|
+**Story 状态:** ready-for-dev
|