|
|
@@ -0,0 +1,289 @@
|
|
|
+# Story 3.1: 开发文件上传工具函数
|
|
|
+
|
|
|
+Status: ready-for-dev
|
|
|
+
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
+
|
|
|
+## 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
|
|
|
+
|
|
|
+- [ ] **Task 1: 更新 types.ts 中的 FileUploadOptions 定义** (AC: #6)
|
|
|
+ - [ ] Subtask 1.1: 修改 `FileUploadOptions` 接口,添加 `fixturesDir` 和 `waitForUpload` 选项
|
|
|
+ - [ ] Subtask 1.2: 移除 `@beta` 标记,因为此功能即将实现
|
|
|
+
|
|
|
+- [ ] **Task 2: 实现 `src/file-upload.ts` 核心函数** (AC: #1, #2, #3, #4, #5)
|
|
|
+ - [ ] Subtask 2.1: 实现 `uploadFileToField()` 主函数
|
|
|
+ - [ ] Subtask 2.2: 实现 fixtures 路径解析逻辑
|
|
|
+ - [ ] Subtask 2.3: 实现文件存在性检查
|
|
|
+ - [ ] Subtask 2.4: 实现错误处理(文件不存在、选择器无效、超时)
|
|
|
+ - [ ] Subtask 2.5: 添加 `console.debug()` 日志
|
|
|
+
|
|
|
+- [ ] **Task 3: 更新 `src/index.ts` 导出** (AC: #2)
|
|
|
+ - [ ] Subtask 3.1: 导出 `uploadFileToField` 函数
|
|
|
+ - [ ] Subtask 3.2: 导出 `FileUploadOptions` 类型
|
|
|
+
|
|
|
+- [ ] **Task 4: 添加 JSDoc 注释** (NFR25-NFR40)
|
|
|
+ - [ ] Subtask 4.1: 添加完整的 JSDoc(@param, @throws, @example)
|
|
|
+ - [ ] Subtask 4.2: 内部辅助函数使用 `@internal` 标记
|
|
|
+
|
|
|
+- [ ] **Task 5: 创建 fixtures 测试文件** (AC: #1)
|
|
|
+ - [ ] Subtask 5.1: 在 `packages/e2e-test-utils/tests/fixtures/images/` 创建测试图片占位文件
|
|
|
+ - [ ] Subtask 5.2: 在 `web/tests/fixtures/images/` 创建测试图片占位文件
|
|
|
+
|
|
|
+## 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<void>
|
|
|
+
|
|
|
+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<void> {
|
|
|
+ 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 结构假设必须验证、选择器策略优先级
|
|
|
+
|
|
|
+### 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. Epic 3 状态已更新为 `in-progress`
|
|
|
+2. Story 文件已创建在 `_bmad-output/implementation-artifacts/3-1-file-upload-tool.md`
|
|
|
+
|
|
|
+### File List
|
|
|
+
|
|
|
+- `_bmad-output/implementation-artifacts/3-1-file-upload-tool.md` (新建)
|
|
|
+- `_bmad-output/implementation-artifacts/sprint-status.yaml` (已更新 epic-3 状态)
|