3-1-file-upload-tool.md 14 KB

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. 错误时提供清晰消息

    • 使用 E2ETestErrorErrorContext
    • 错误消息包含:文件路径、选择器、失败原因、修复建议
    • 区分文件不存在和选择器无效两种错误场景
  5. 操作在 5 秒内完成(NFR9)

    • 默认超时 5000ms,可通过 FileUploadOptions.timeout 自定义
  6. 配置对象继承 BaseOptions

    • FileUploadOptions extends BaseOptions
    • 支持 timeout 配置

Tasks / Subtasks

  • [x] Task 1: 更新 types.ts 中的 FileUploadOptions 定义 (AC: #6)

    • Subtask 1.1: 修改 FileUploadOptions 接口,添加 fixturesDirwaitForUpload 选项
    • Subtask 1.2: 移除 @beta 标记,因为此功能即将实现
  • [x] 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() 日志
  • [x] Task 3: 更新 src/index.ts 导出 (AC: #2)

    • Subtask 3.1: 导出 uploadFileToField 函数
    • Subtask 3.2: 导出 FileUploadOptions 类型
  • [x] Task 4: 添加 JSDoc 注释 (NFR25-NFR40)

    • Subtask 4.1: 添加完整的 JSDoc(@param, @throws, @example
    • Subtask 4.2: 内部辅助函数使用 @internal 标记
  • [x] Task 5: 安装依赖并验证类型 (AC: #1)

    • Subtask 5.1: 添加 @types/node 到 devDependencies
    • Subtask 5.2: 运行 TypeScript 类型检查通过
  • [x] Task 6: 修改 MinioUploader 组件使其可测试 (AC: #2)

    • Subtask 6.1: 添加隐藏的静态文件输入框到 DOM 中
    • Subtask 6.2: 添加 testId prop 支持唯一标识符
    • Subtask 6.3: 修改 FileSelector 传递 testId
    • Subtask 6.4: 修改 PhotoUploadField 生成唯一 testId
    • Subtask 6.5: 运行类型检查验证所有组件

Dev Notes

Epic 3 背景与目标

Epic 3: 文件上传工具开发与验证

遵循 Epic 2 的成功模式,开发文件上传工具并在真实 E2E 测试中验证,解决当前测试超时阻塞问题。

模式: 工具开发 → 真实 E2E 测试验证 → 问题修复 → 稳定性验证

Epic 2 关键经验(必须应用):

  1. 单元测试无法发现真实 DOM 问题,必须添加集成测试
  2. DOM 结构假设必须验证,不能基于理想模型开发
  3. 真实 E2E 测试不可替代

当前测试超时问题:

  • 残疾人管理测试在文件上传阶段超时(60秒)
  • 现有的 uploadPhoto() 方法使用复杂的 DOM 操作
  • 需要简化文件上传流程

技术规范

函数签名

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 的实现模式:

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

测试使用方式

// 上传第一张照片
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 接口,添加 fixturesDirwaitForUpload 选项
  2. ✅ 实现 uploadFileToField() 核心函数,包含完整错误处理
  3. ✅ 更新 src/index.ts 导出 uploadFileToFieldFileUploadOptions
  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/ - 创建测试图片占位文件