# Story 3.1: 开发文件上传工具函数

Status: done

<!-- 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

- [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<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 结构假设必须验证、选择器策略优先级

### ✅ 解决方案：修改 MinioUploader 支持静态文件输入框

**问题**：`MinioUploader` 原本使用动态创建的文件输入框，无法用于 E2E 测试。

**解决方案**：修改组件架构，添加隐藏的静态文件输入框：

1. **MinioUploader.tsx 修改**：
   - 添加 `testId` prop 用于唯一标识
   - 使用 `useRef` 保存文件输入框引用
   - 在 DOM 中添加隐藏的 `<input type="file">` 元素
   - 所有上传按钮点击时使用 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');
```

**适用场景**：
- ✅ 标准静态 `<input type="file">` 元素
- ✅ 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/` - 创建测试图片占位文件
