Status: done
作为 E2E 测试开发者,
我想要在真实的 E2E 测试中使用 uploadFileToField() 工具函数,
以便验证文件上传工具在实际场景中的可用性和稳定性。
Given Story 3.1 已实现 uploadFileToField() 函数(含 UI 组件 testId 支持)
Given Story 3.2 已完成单元测试(覆盖率 91.66%)
Given PhotoUploadField 组件已添加 testId prop(testId={"photo-upload-${index}"})
When 在 web/tests/e2e/ 中创建或更新测试文件
Then 验收标准如下:
创建独立的文件上传 E2E 测试文件
web/tests/e2e/specs/admin/file-upload-validation.spec.tsuploadFileToField() 在真实场景中的可用性测试场景包括
使用真实 fixtures 文件
web/tests/fixtures/images/ 中的真实图片文件测试在真实浏览器中运行
收集反馈并记录问题
[x] Task 1: 创建文件上传 E2E 测试文件 (AC: #1, #2)
file-upload-validation.spec.ts 测试文件uploadFileToField 函数[x] Task 2: 实现基本文件上传测试 (AC: #2, #3)
[data-testid="photo-upload-0"][x] Task 3: 实现多文件上传测试 (AC: #2)
photo-upload-1, photo-upload-2)[x] Task 4: 实现 E2E 集成测试(完整表单场景) (AC: #2, #4)
[x] Task 5: 运行测试并收集结果 (AC: #4, #5)
pnpm test:e2e:chromium file-upload-validation[ ] Task 6: 更新 disability-person.page.ts (可选 - AC: #4)
uploadPhoto() 方法使用 uploadFileToField()[x] Task 7: 编写测试报告 (AC: #5)
Epic 3: 文件上传工具开发与验证
遵循 Epic 2 的成功模式,开发文件上传工具并在真实 E2E 测试中验证,解决当前测试超时阻塞问题。
模式: 工具开发 → 真实 E2E 测试验证 → 问题修复 → 稳定性验证
当前进度:
uploadFileToField() 函数已实现(含 UI 组件 testId 支持)单元测试的局限性(来自 Epic 2 回顾):
单元测试无法发现真实 DOM 问题
真实 E2E 测试不可替代
本 Story 的定位
disability-person.page.ts 中的 uploadPhoto() 方法(第 176-205 行):
async uploadPhoto(photoType: string, fileName: string) {
// ❌ 问题 1: 使用复杂的 XPath 查找
const photoSection = this.page.locator('text=' + photoType).first();
await photoSection.scrollIntoViewIfNeeded();
const uploadButton = photoSection.locator('xpath=ancestor::div[contains(@class, "space-y-")]').first()
.getByRole('button', { name: /上传/ }).first();
// ❌ 问题 2: 使用 evaluateHandle 查找隐藏的 file input
const fileInput = await uploadButton.evaluateHandle((el: any) => {
const input = el.querySelector('input[type="file"]');
return input;
});
// ❌ 问题 3: 创建假图片文件
const file = {
name: fileName,
mimeType: 'image/jpeg',
buffer: Buffer.from('fake image content')
};
await fileInput.uploadFile(file as any);
}
问题总结:
evaluateHandle 增加复杂度函数签名(已实现):
import { uploadFileToField } from '@d8d/e2e-test-utils';
await uploadFileToField(
page, // Playwright Page 对象
'[data-testid="photo-upload-0"]', // 选择器
'sample-id-card.jpg', // 文件名(相对于 web/tests/fixtures)
{ timeout: 5000 } // 可选配置
);
关键特性:
page.locator().setInputFiles() API(Playwright 标准)web/tests/fixtures)组件结构(PhotoUploadField.tsx 第 172 行):
<FileSelector
value={photo.fileId}
onChange={(fileId) => handleFileIdChange(index, fileId)}
testId={`photo-upload-${index}`} // ← 关键:生成唯一 testId
/>
FileSelector → MinioUploader 传递链:
PhotoUploadField (testId={"photo-upload-${index}"})
↓
FileSelector (testId prop)
↓
MinioUploader (添加隐藏的 <input type="file" data-testid={testId}>)
测试使用方式:
// 上传第 1 张照片(身份证照片)
await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
// 上传第 2 张照片(残疾证照片)
await uploadFileToField(page, '[data-testid="photo-upload-1"]', 'sample-disability-card.jpg');
测试目标: 验证 uploadFileToField() 能成功上传单个文件
test('应该成功上传单张照片', async ({ disabilityPersonPage, page }) => {
const timestamp = Date.now();
// 1. 打开对话框并填写基本信息
await disabilityPersonPage.openCreateDialog();
await disabilityPersonPage.fillBasicForm({
name: `文件上传测试_${timestamp}`,
gender: '男',
idCard: '420101199001011234',
disabilityId: '51100119900101',
disabilityType: '视力残疾',
disabilityLevel: '一级',
phone: '13800138000',
idAddress: '湖北省武汉市测试街道1号',
province: '湖北省',
city: '武汉市'
});
// 2. 滚动到照片区域并添加一张照片
await disabilityPersonPage.scrollToSection('照片');
await page.getByTestId('add-photo-button').click();
// 3. 使用 uploadFileToField 上传文件
await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
// 4. 验证上传成功(检查预览或文件名显示)
const preview = page.locator('[data-testid="photo-upload-0"]').locator('img');
await expect(preview).toBeVisible({ timeout: 5000 });
});
测试目标: 验证连续上传多张文件
test('应该成功上传多张照片', async ({ disabilityPersonPage, page }) => {
const timestamp = Date.now();
// 1. 打开对话框并填写基本信息
await disabilityPersonPage.openCreateDialog();
await disabilityPersonPage.fillBasicForm({...});
// 2. 滚动到照片区域
await disabilityPersonPage.scrollToSection('照片');
// 3. 添加三张照片
await page.getByTestId('add-photo-button').click();
await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
await page.getByTestId('add-photo-button').click();
await uploadFileToField(page, '[data-testid="photo-upload-1"]', 'sample-disability-card.jpg');
await page.getByTestId('add-photo-button').click();
await uploadFileToField(page, '[data-testid="photo-upload-2"]', 'sample-id-card.jpg');
// 4. 验证所有照片都上传成功
const previews = page.locator('[data-testid^="photo-upload-"]').locator('img');
await expect(previews).toHaveCount(3);
});
测试目标: 验证文件上传后的表单能成功提交
test('应该成功提交包含照片的表单', async ({ disabilityPersonPage, page }) => {
const timestamp = Date.now();
// 1. 打开对话框并填写基本信息
await disabilityPersonPage.openCreateDialog();
await disabilityPersonPage.fillBasicForm({...});
// 2. 上传照片
await disabilityPersonPage.scrollToSection('照片');
await page.getByTestId('add-photo-button').click();
await uploadFileToField(page, '[data-testid="photo-upload-0"]', 'sample-id-card.jpg');
// 3. 提交表单
const result = await disabilityPersonPage.submitForm();
// 4. 验证提交成功
expect(result.hasSuccess).toBe(true);
expect(result.hasError).toBe(false);
// 5. 验证数据保存
await disabilityPersonPage.waitForDialogClosed();
await page.reload();
await disabilityPersonPage.goto();
await disabilityPersonPage.searchByName(`文件上传测试_${timestamp}`);
const personExists = await disabilityPersonPage.personExists(`文件上传测试_${timestamp}`);
expect(personExists).toBe(true);
});
测试文件位置:
web/tests/
├── e2e/
│ ├── specs/
│ │ └── admin/
│ │ ├── file-upload-validation.spec.ts # 本 Story 创建
│ │ └── disability-person-complete.spec.ts # 现有测试
│ ├── fixtures/
│ │ └── images/
│ │ ├── sample-id-card.jpg # 已存在
│ │ └── sample-disability-card.jpg # 已存在
│ └── pages/
│ └── admin/
│ └── disability-person.page.ts # 现有 Page Object
包依赖:
// web/tests/e2e/specs/admin/file-upload-validation.spec.ts
import { uploadFileToField } from '@d8d/e2e-test-utils';
运行本 Story 的测试:
# 从项目根目录
cd web && pnpm test:e2e:chromium file-upload-validation
# 快速失败模式(推荐调试时使用)
timeout 60 pnpm test:e2e:chromium file-upload-validation
运行完整的残疾人管理测试(验证集成):
cd web && pnpm test:e2e:chromium disability-person-complete
架构文档:
_bmad-output/planning-artifacts/architecture.md - E2E 测试标准、三层测试策略E2E 测试标准:
docs/standards/e2e-radix-testing.md - 文件上传测试标准Epic 3 完整需求:
_bmad-output/implementation-artifacts/ 中Epic 2 回顾(关键经验):
_bmad-output/implementation-artifacts/epic-2-retrospective.md - 真实 E2E 测试的价值TypeScript + Playwright 陷阱:
architecture.md 第 533-657 行 - DOM 结构假设必须验证已实现的源文件参考:
Monorepo 结构对齐:
web/tests/e2e/ 目录@d8d/e2e-test-utils workspace 包web/tests/fixtures/文件组织:
specs/admin/ 目录.spec.ts 后缀命名(Playwright 约定)pages/admin/ 目录源文档引用:
现有测试参考:
Claude Opus 4 (claude-opus-4-5-20251101)
代码审查发现的问题及修复记录:
测试文件未提交到 Git
git add 将文件纳入版本控制场景 3 测试不验证表单提交成功
file-upload-validation.spec.ts:263-296expect(result.hasSuccess).toBe(true) 断言行数记录不准确
硬编码魔法超时值
page.waitForTimeout(300/500/2000) 导致测试缓慢且不稳定expect().toBeVisible() 条件等待,移除大部分魔法超时console.log 不符合项目规范
console.debug 会显示console.log 替换为 console.debugfixturesDir 参数重复传递
{ fixturesDir: 'tests/fixtures' }UPLOAD_OPTIONS 常量场景 5 只测试 JPG 格式
expect(value, 'description').toBe(true)本 Story 已完成开发,记录如下:
新增文件:
web/tests/e2e/specs/admin/file-upload-validation.spec.ts (416 行)
核心测试通过:
uploadFileToField() 函数在真实 E2E 环境中正常工作关键发现:
tests/fixtures(相对于 web/ 目录)问题 1: 选择器找不到元素
问题 2: 文件路径解析错误
web/tests/fixtures,但从 web/ 目录运行时会导致路径重复{ fixturesDir: 'tests/fixtures' } 选项问题 3: TypeScript 变量重复声明
已验证功能:
待验证(因测试复杂度):
单元测试(Story 3.2)覆盖:
本 Story E2E 测试新增验证:
本 Story 已创建/修改的文件:
新增文件:
web/tests/e2e/specs/admin/file-upload-validation.spec.ts (414 行) - 文件上传 E2E 验证测试
uploadFileToField 函数已修改文件:
_bmad-output/implementation-artifacts/3-3-upload-e2e-integration.md - 本 Story 文件
_bmad-output/implementation-artifacts/sprint-status.yaml - Sprint 状态跟踪
未修改(标记为可选):
web/tests/e2e/pages/admin/disability-person.page.ts - 更新 uploadPhoto() 方法
Story 创建日期: 2026-01-10 Story 完成日期: 2026-01-10 Story 状态: review