# Story 3.1: 开发文件上传工具函数 Status: done ## Story 作为测试开发者, 我想要使用 `uploadFileToField()` 函数上传文件, 以便测试照片上传、文档上传等功能。 ## Acceptance Criteria **Given** Epic 1 的类型定义已存在(`BaseOptions`, `E2ETestError`, `ErrorContext`, `DEFAULT_TIMEOUTS`) **When** 实现 `src/file-upload.ts` 中的 `uploadFileToField(page, selector, fileName)` 函数 **Then** 验收标准如下: 1. **函数从 fixtures 目录加载测试文件** - 支持默认 fixtures 目录:`web/tests/fixtures/` - 支持通过 `FileUploadOptions.fixturesDir` 自定义 fixtures 目录 - 文件名相对于 fixtures 目录解析 2. **使用 Playwright 的 `setInputFiles()` API** - 使用 `page.locator(selector).setInputFiles()` 方法 - 支持 `data-testid`, `aria-label`, CSS 选择器等多种选择器 3. **支持相对路径(相对于 fixtures 目录)** - 文件名如 `'sample-id-card.jpg'` 自动在 fixtures 目录中查找 - 支持子目录如 `'images/sample-id-card.jpg'` 4. **错误时提供清晰消息** - 使用 `E2ETestError` 和 `ErrorContext` - 错误消息包含:文件路径、选择器、失败原因、修复建议 - 区分文件不存在和选择器无效两种错误场景 5. **操作在 5 秒内完成(NFR9)** - 默认超时 5000ms,可通过 `FileUploadOptions.timeout` 自定义 6. **配置对象继承 `BaseOptions`** - `FileUploadOptions extends BaseOptions` - 支持 `timeout` 配置 ## Tasks / Subtasks - [x] **Task 1: 更新 types.ts 中的 FileUploadOptions 定义** (AC: #6) - [x] Subtask 1.1: 修改 `FileUploadOptions` 接口,添加 `fixturesDir` 和 `waitForUpload` 选项 - [x] Subtask 1.2: 移除 `@beta` 标记,因为此功能即将实现 - [x] **Task 2: 实现 `src/file-upload.ts` 核心函数** (AC: #1, #2, #3, #4, #5) - [x] Subtask 2.1: 实现 `uploadFileToField()` 主函数 - [x] Subtask 2.2: 实现 fixtures 路径解析逻辑 - [x] Subtask 2.3: 实现文件存在性检查 - [x] Subtask 2.4: 实现错误处理(文件不存在、选择器无效、超时) - [x] Subtask 2.5: 添加 `console.debug()` 日志 - [x] **Task 3: 更新 `src/index.ts` 导出** (AC: #2) - [x] Subtask 3.1: 导出 `uploadFileToField` 函数 - [x] Subtask 3.2: 导出 `FileUploadOptions` 类型 - [x] **Task 4: 添加 JSDoc 注释** (NFR25-NFR40) - [x] Subtask 4.1: 添加完整的 JSDoc(@param, @throws, @example) - [x] Subtask 4.2: 内部辅助函数使用 `@internal` 标记 - [x] **Task 5: 安装依赖并验证类型** (AC: #1) - [x] Subtask 5.1: 添加 `@types/node` 到 devDependencies - [x] Subtask 5.2: 运行 TypeScript 类型检查通过 - [x] **Task 6: 修改 MinioUploader 组件使其可测试** (AC: #2) - [x] Subtask 6.1: 添加隐藏的静态文件输入框到 DOM 中 - [x] Subtask 6.2: 添加 `testId` prop 支持唯一标识符 - [x] Subtask 6.3: 修改 FileSelector 传递 testId - [x] Subtask 6.4: 修改 PhotoUploadField 生成唯一 testId - [x] Subtask 6.5: 运行类型检查验证所有组件 ## Dev Notes ### Epic 3 背景与目标 **Epic 3: 文件上传工具开发与验证** 遵循 Epic 2 的成功模式,开发文件上传工具并在真实 E2E 测试中验证,解决当前测试超时阻塞问题。 **模式:** 工具开发 → 真实 E2E 测试验证 → 问题修复 → 稳定性验证 **Epic 2 关键经验(必须应用):** 1. 单元测试无法发现真实 DOM 问题,必须添加集成测试 2. DOM 结构假设必须验证,不能基于理想模型开发 3. 真实 E2E 测试不可替代 **当前测试超时问题:** - 残疾人管理测试在文件上传阶段超时(60秒) - 现有的 `uploadPhoto()` 方法使用复杂的 DOM 操作 - 需要简化文件上传流程 ### 技术规范 #### 函数签名 ```typescript export async function uploadFileToField( page: Page, selector: string, fileName: string, options?: FileUploadOptions ): Promise export interface FileUploadOptions extends BaseOptions { /** fixtures 目录路径,默认为 'tests/fixtures' */ fixturesDir?: string; /** 是否等待上传完成,默认为 true */ waitForUpload?: boolean; } ``` #### 实现要点 1. **Fixtures 路径解析** - 默认 fixtures 目录:`tests/fixtures/`(相对于测试运行目录) - 支持 `options.fixturesDir` 自定义路径 - 使用 Node.js `path.join()` 和 `path.resolve()` 解析路径 2. **文件存在性检查** - 使用 `fs.existsSync()` 检查文件是否存在 - 如果文件不存在,抛出 `E2ETestError` 包含完整路径和建议 3. **文件上传** - 使用 `page.locator(selector).setInputFiles(filePath)` - 支持多种选择器策略:`data-testid` > `aria-label` > CSS selector 4. **错误处理** - 文件不存在:提供完整文件路径和可能的原因 - 选择器无效:提供选择器字符串和页面当前状态 - 超时:使用 `options.timeout` 或默认 5000ms 5. **日志输出** - 使用 `console.debug()` 输出调试信息(仅 Vitest 中可见) - 包括:文件路径、选择器、上传状态 ### 项目结构说明 **包结构:** ``` packages/e2e-test-utils/ ├── src/ │ ├── index.ts # 主导出,需要更新 │ ├── types.ts # 共享类型定义,需要更新 FileUploadOptions │ ├── errors.ts # 错误类和错误处理 │ ├── constants.ts # 常量定义(超时) │ ├── radix-select.ts # Radix UI Select 工具(参考实现) │ └── file-upload.ts # 文件上传工具(需要实现) ├── tests/ │ ├── fixtures/ │ │ └── images/ │ │ ├── sample-id-card.jpg # 测试图片占位文件 │ │ └── sample-disability-card.jpg │ └── unit/ │ └── file-upload.test.ts # 单元测试(下个 story) ``` **web/tests/fixtures 结构(需要创建):** ``` web/tests/fixtures/ ├── images/ │ ├── sample-id-card.jpg │ └── sample-disability-card.jpg ``` ### 代码模式参考 参考 `radix-select.ts` 的实现模式: ```typescript import type { Page } from "@playwright/test"; import type { FileUploadOptions } from "./types"; import { throwError } from "./errors"; import { DEFAULT_TIMEOUTS } from "./constants"; /** * 上传文件到指定输入框 * * @description * 从 fixtures 目录加载测试文件并上传到指定的文件输入框。 * ... */ export async function uploadFileToField( page: Page, selector: string, fileName: string, options?: FileUploadOptions ): Promise { console.debug(`[uploadFileToField] 开始上传: selector="${selector}", fileName="${fileName}"`); // 1. 合并默认配置 const config = { timeout: options?.timeout ?? DEFAULT_TIMEOUTS.static, fixturesDir: options?.fixturesDir ?? 'tests/fixtures', waitForUpload: options?.waitForUpload ?? true }; // 2. 解析文件路径 const filePath = resolveFixturePath(fileName, config.fixturesDir); // 3. 检查文件是否存在 if (!fs.existsSync(filePath)) { throwError({ operation: 'uploadFileToField', target: `文件 "${fileName}"`, expected: `文件存在于 ${filePath}`, suggestion: '检查文件名是否正确,或确认文件已添加到 fixtures 目录' }); } // 4. 查找文件输入框并上传 // ... 实现 console.debug(`[uploadFileToField] 上传完成`); } ``` ### 测试场景(用于 Story 3.3) 将在 `web/tests/e2e/specs/admin/disability-person-complete.spec.ts` 中验证: 1. **身份证照片上传** - 测试基本文件上传功能 2. **残疾证照片上传** - 测试不同文件类型 3. **个人照片上传** - 测试多文件场景 4. **文件不存在场景** - 测试错误处理 ### 参考文档 **架构文档:** - `_bmad-output/planning-artifacts/architecture.md` - 包结构、API 设计模式、错误处理策略 **E2E 测试标准:** - `docs/standards/e2e-radix-testing.md` - 文件上传测试标准(第 155-198 行) **Epic 3 完整需求:** - `_bmad-output/planning-artifacts/epics.md` - Epic 3 和 Story 3.1 详细需求(第 710-777 行) **Epic 2 回顾(关键经验):** - `_bmad-output/implementation-artifacts/epic-2-retrospective.md` - DOM 结构假设必须验证 **TypeScript + Playwright 陷阱:** - `architecture.md` 第 533-657 行 - DOM 结构假设必须验证、选择器策略优先级 ### ✅ 解决方案:修改 MinioUploader 支持静态文件输入框 **问题**:`MinioUploader` 原本使用动态创建的文件输入框,无法用于 E2E 测试。 **解决方案**:修改组件架构,添加隐藏的静态文件输入框: 1. **MinioUploader.tsx 修改**: - 添加 `testId` prop 用于唯一标识 - 使用 `useRef` 保存文件输入框引用 - 在 DOM 中添加隐藏的 `` 元素 - 所有上传按钮点击时使用 ref 触发 2. **FileSelector.tsx 修改**: - 添加 `testId` prop - 传递 `testId` 给 MinioUploader 3. **PhotoUploadField.tsx 修改**: - 为每个照片项生成唯一的 `testId={photo-upload-${index}}` **测试使用方式**: ```typescript // 上传第一张照片 await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'id-card.jpg'); // 上传第二张照片 await uploadFileToField(page, '[data-testid="photo-upload-1"]', 'disability-card.jpg'); ``` **适用场景**: - ✅ 标准静态 `` 元素 - ✅ FileSelector/MinioUploader 组件(通过 testId) - ✅ 任何在 DOM 中可见的文件输入框 ### Project Structure Notes **Monorepo 结构对齐:** - 包位于 `packages/e2e-test-utils/` - 使用 pnpm workspace 协议 - 与现有 `@d8d/shared-test-util`(后端集成测试)分离 **文件组织:** - 源文件按功能分组:`file-upload.ts` 用于文件上传 - 共享代码集中在:`types.ts`, `errors.ts`, `constants.ts` - 主导出使用 `index.ts`,支持 tree-shaking **无冲突检测:** - 无命名冲突(新文件 `file-upload.ts`) - 无类型冲突(扩展现有 `FileUploadOptions`) - 无导出冲突(新增导出) ### References **源文档引用:** - [Source: _bmad-output/planning-artifacts/epics.md#Epic-3-Story-3.1] - [Source: _bmad-output/planning-artifacts/architecture.md#Package-Structure] - [Source: docs/standards/e2e-radix-testing.md#文件上传测试] **现有代码参考:** - [Source: packages/e2e-test-utils/src/radix-select.ts] - 实现模式参考 - [Source: packages/e2e-test-utils/src/types.ts] - 类型定义 - [Source: packages/e2e-test-utils/src/errors.ts] - 错误处理 - [Source: packages/e2e-test-utils/src/constants.ts] - 超时常量 - [Source: web/tests/e2e/pages/admin/disability-person.page.ts#uploadPhoto] - 现有文件上传方法(第 176-205 行) ## Dev Agent Record ### Agent Model Used Claude Opus 4 (claude-opus-4-5-20251101) ### Debug Log References ### Completion Notes List 1. ✅ 更新 `FileUploadOptions` 接口,添加 `fixturesDir` 和 `waitForUpload` 选项 2. ✅ 实现 `uploadFileToField()` 核心函数,包含完整错误处理 3. ✅ 更新 `src/index.ts` 导出 `uploadFileToField` 和 `FileUploadOptions` 4. ✅ 添加完整的 JSDoc 注释(@param, @throws, @example, @internal) 5. ✅ 添加 `@types/node` 依赖,TypeScript 类型检查通过 6. ✅ 修改 MinioUploader 组件添加 `testId` prop 和隐藏的静态文件输入框 7. ✅ 修改 FileSelector 组件传递 `testId` 给 MinioUploader 8. ✅ 修改 PhotoUploadField 组件生成唯一的 `testId={photo-upload-${index}}` ### File List **e2e-test-utils 包修改:** - `packages/e2e-test-utils/src/types.ts` - 更新 FileUploadOptions 接口 - `packages/e2e-test-utils/src/index.ts` - 添加 uploadFileToField 导出 - `packages/e2e-test-utils/src/file-upload.ts` - 新建文件上传工具函数 - `packages/e2e-test-utils/package.json` - 添加 @types/node 依赖 **file-management-ui 包修改:** - `packages/file-management-ui/src/components/MinioUploader.tsx` - 添加 testId prop、隐藏文件输入框 - `packages/file-management-ui/src/components/FileSelector.tsx` - 添加 testId prop 并传递 **disability-person-management-ui 包修改:** - `allin-packages/disability-person-management-ui/src/components/PhotoUploadField.tsx` - 生成唯一 testId ### Change Log **2026-01-10** - 完成 Story 3.1 实现(包含架构修改) - 实现 `uploadFileToField()` 通用文件上传工具函数 - 添加 `@types/node` 依赖支持 Node.js API 类型 - **架构改进**:修改 MinioUploader/FileSelector/PhotoUploadField 组件,使其支持 E2E 测试 - 添加 `testId` prop 机制,支持页面中多个上传组件的测试定位 **2026-01-10** - 代码审查修复 - ✅ 修复默认 fixtures 路径为 `web/tests/fixtures`(符合 AC #1) - ✅ 创建测试图片占位文件(sample-id-card.jpg, sample-disability-card.jpg) - ✅ 加强路径验证防止路径遍历攻击 - ✅ 改进选择器无效错误处理,提供更详细的调试建议 --- ## Code Review Record **Reviewer:** Claude (code-review workflow) **Review Date:** 2026-01-10 **Original Status:** review **Final Status:** done ### Issues Found and Fixed | Severity | Issue | Status | Fix Details | |----------|-------|--------|-------------| | HIGH | 默认 fixtures 路径错误 | ✅ Fixed | 修改为 `web/tests/fixtures` | | HIGH | 测试 fixtures 目录为空 | ✅ Fixed | 创建 sample-id-card.jpg 和 sample-disability-card.jpg | | HIGH | 错误场景区分不清晰 | ✅ Fixed | 改进选择器错误处理,提供详细调试建议 | | MEDIUM | 路径遍历漏洞风险 | ✅ Fixed | 添加双重验证确保解析路径在 fixtures 目录内 | | LOW | 缺少单元测试 | ⏳ Deferred | 计划在 Story 3.2 实现 | ### Files Modified During Review 1. `packages/e2e-test-utils/src/file-upload.ts` - 修复默认路径、加强验证、改进错误处理 2. `packages/e2e-test-utils/src/types.ts` - 更新 JSDoc 注释 3. `web/tests/fixtures/images/` - 创建测试图片占位文件