|
@@ -0,0 +1,355 @@
|
|
|
|
|
+# Story 3.5: 文件上传稳定性验证
|
|
|
|
|
+
|
|
|
|
|
+Status: ready-for-dev
|
|
|
|
|
+
|
|
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
|
+
|
|
|
|
|
+## Story
|
|
|
|
|
+
|
|
|
|
|
+作为测试开发者,
|
|
|
|
|
+我想要验证文件上传工具的稳定性,
|
|
|
|
|
+以便确保工具可以可靠地在 E2E 测试中使用。
|
|
|
|
|
+
|
|
|
|
|
+## Acceptance Criteria
|
|
|
|
|
+
|
|
|
|
|
+**Given** Story 3.1 已实现 `uploadFileToField()` 函数
|
|
|
|
|
+**Given** Story 3.2 已完成单元测试(覆盖率 91.66%)
|
|
|
|
|
+**Given** Story 3.3 已在真实 E2E 测试中验证文件上传工具(场景 1 通过)
|
|
|
|
|
+**Given** Story 3.4 已修复所有发现的工具 bug(Select 工具问题已解决)
|
|
|
|
|
+
|
|
|
|
|
+**When** 连续运行文件上传相关测试 10 次
|
|
|
|
|
+
|
|
|
|
|
+**Then** 验收标准如下:
|
|
|
|
|
+
|
|
|
|
|
+1. **测试通过率 100%**
|
|
|
|
|
+ - 10/10 次运行全部通过
|
|
|
|
|
+ - 无 flaky 失败
|
|
|
|
|
+ - 无超时失败
|
|
|
|
|
+
|
|
|
|
|
+2. **性能指标达标**
|
|
|
|
|
+ - 平均执行时间 ≤ 5 秒/文件
|
|
|
|
|
+ - 最大执行时间 < 10 秒/文件
|
|
|
|
|
+ - 无明显性能衰减
|
|
|
|
|
+
|
|
|
|
|
+3. **多文件上传稳定性**
|
|
|
|
|
+ - 连续上传多张照片场景稳定
|
|
|
|
|
+ - 不同文件类型上传正常
|
|
|
|
|
+ - 上传后的预览显示正常
|
|
|
|
|
+
|
|
|
|
|
+4. **稳定性测试报告**
|
|
|
|
|
+ - 记录每次运行的详细结果
|
|
|
|
|
+ - 统计通过率、平均时间、失败原因
|
|
|
|
|
+ - 确认是否达到 Epic 3 完成标准
|
|
|
|
|
+
|
|
|
|
|
+## Tasks / Subtasks
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Task 1: 准备稳定性测试环境** (AC: #1)
|
|
|
|
|
+ - [ ] Subtask 1.1: 确认 Select 工具修复已生效
|
|
|
|
|
+ - [ ] Subtask 1.2: 确认文件上传测试文件完整
|
|
|
|
|
+ - [ ] Subtask 1.3: 确认 fixtures 文件存在
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Task 2: 执行稳定性测试(10次连续运行)** (AC: #1, #2)
|
|
|
|
|
+ - [ ] Subtask 2.1: 运行场景 1(单文件上传)10 次
|
|
|
|
|
+ - [ ] Subtask 2.2: 运行场景 2(多文件上传)10 次
|
|
|
|
|
+ - [ ] Subtask 2.3: 记录每次运行的通过率和时间
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Task 3: 分析测试结果** (AC: #1, #2, #3)
|
|
|
|
|
+ - [ ] Subtask 3.1: 统计总体通过率
|
|
|
|
|
+ - [ ] Subtask 3.2: 分析失败原因(如果有)
|
|
|
|
|
+ - [ ] Subtask 3.3: 测量平均执行时间
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Task 4: 修复发现的稳定性问题** (AC: #1)
|
|
|
|
|
+ - [ ] Subtask 4.1: 分析 flaky 失败的根本原因
|
|
|
|
|
+ - [ ] Subtask 4.2: 修复工具或测试代码
|
|
|
|
|
+ - [ ] Subtask 4.3: 重新运行验证修复
|
|
|
|
|
+
|
|
|
|
|
+- [ ] **Task 5: 生成稳定性测试报告** (AC: #4)
|
|
|
|
|
+ - [ ] Subtask 5.1: 记录测试统计数据
|
|
|
|
|
+ - [ ] Subtask 5.2: 确认 Epic 3 完成标准
|
|
|
|
|
+ - [ ] Subtask 5.3: 为 Epic 3 回顾提供数据
|
|
|
|
|
+
|
|
|
|
|
+## Dev Notes
|
|
|
|
|
+
|
|
|
|
|
+### Epic 3 背景与目标
|
|
|
|
|
+
|
|
|
|
|
+**Epic 3: 文件上传工具开发与验证**
|
|
|
|
|
+
|
|
|
|
|
+遵循 Epic 2 的成功模式,开发文件上传工具并在真实 E2E 测试中验证,解决当前测试超时阻塞问题。
|
|
|
|
|
+
|
|
|
|
|
+**模式:** 工具开发 → 真实 E2E 测试验证 → 问题修复 → 稳定性验证
|
|
|
|
|
+
|
|
|
|
|
+**当前进度:**
|
|
|
|
|
+- Story 3.1: ✅ 已完成 - `uploadFileToField()` 函数已实现(含 UI 组件 testId 支持)
|
|
|
|
|
+- Story 3.2: ✅ 已完成 - 单元测试(覆盖率 91.66%)
|
|
|
|
|
+- Story 3.3: ✅ 已完成 - 在真实 E2E 测试中验证(场景 1 通过)
|
|
|
|
|
+- Story 3.4: ✅ 已完成 - 收集反馈并修复问题(修复了 Select 工具处理带 `*` 标签的问题)
|
|
|
|
|
+- Story 3.5: 🔄 本 Story - 文件上传稳定性验证(10次连续运行)
|
|
|
|
|
+
|
|
|
|
|
+### Epic 2 稳定性验证经验
|
|
|
|
|
+
|
|
|
|
|
+**来自 Epic 2 回顾的关键经验:**
|
|
|
|
|
+
|
|
|
|
|
+1. **稳定性验证是工具可用的前提**
|
|
|
|
|
+ - Epic 2 的 Select 工具连续运行 10 次才验证了稳定性
|
|
|
|
|
+ - 发现了 DOM 结构问题(listbox → option)
|
|
|
|
|
+ - 修复后才达到 100% 通过率
|
|
|
|
|
+
|
|
|
|
|
+2. **10次连续运行标准**
|
|
|
|
|
+ - 10/10 通过 = 100% 稳定性 ✅ 可以进入下一个 Epic
|
|
|
|
|
+ - 9/10 通过 = 90% 稳定性 ⚠️ 需要分析失败原因
|
|
|
|
|
+ - < 9/10 通过 = 稳定性不足 ❌ 需要修复后再验证
|
|
|
|
|
+
|
|
|
|
|
+3. **记录每次运行的详细结果**
|
|
|
|
|
+ - 通过率百分比
|
|
|
|
|
+ - 平均执行时间
|
|
|
|
|
+ - 失败原因分类(工具 bug / 使用错误 / 环境问题)
|
|
|
|
|
+
|
|
|
|
|
+### 从 Story 3.4 继承的修复
|
|
|
|
|
+
|
|
|
|
|
+**Select 工具修复(已完成):**
|
|
|
|
|
+- ✅ 策略 4:改用 `getByText(label, { exact: true })`
|
|
|
|
|
+- ✅ 新增策略 5:处理标签和 `*` 分离的 DOM 结构
|
|
|
|
|
+- ✅ 验证通过:性别 *、残疾类型 *、残疾等级 *、省份 *、城市
|
|
|
|
|
+
|
|
|
|
|
+**文件上传工具状态:**
|
|
|
|
|
+- ✅ 单元测试 36/36 通过
|
|
|
|
|
+- ✅ E2E 场景 1 验证通过(33.8s)
|
|
|
|
|
+- ✅ fixtures 路径解析正确
|
|
|
|
|
+- ✅ testId 架构工作正常
|
|
|
|
|
+
|
|
|
|
|
+### 稳定性测试设计
|
|
|
|
|
+
|
|
|
|
|
+#### 测试场景(基于 Story 3.3)
|
|
|
|
|
+
|
|
|
|
|
+**场景 1: 单文件上传验证**
|
|
|
|
|
+```typescript
|
|
|
|
|
+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 });
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**场景 2: 多文件上传验证**
|
|
|
|
|
+```typescript
|
|
|
|
|
+test('应该成功上传多张照片', async ({ disabilityPersonPage, page }) => {
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 打开对话框并填写基本信息
|
|
|
|
|
+ await disabilityPersonPage.openCreateDialog();
|
|
|
|
|
+ await disabilityPersonPage.fillBasicForm({
|
|
|
|
|
+ name: `多文件稳定性_${timestamp}`,
|
|
|
|
|
+ gender: '女',
|
|
|
|
|
+ idCard: '420101199001011235',
|
|
|
|
|
+ disabilityId: '51100119900102',
|
|
|
|
|
+ disabilityType: '听力残疾',
|
|
|
|
|
+ disabilityLevel: '二级',
|
|
|
|
|
+ phone: '13800138001',
|
|
|
|
|
+ idAddress: '湖北省武汉市测试街道2号',
|
|
|
|
|
+ province: '湖北省',
|
|
|
|
|
+ city: '武汉市'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 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);
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 稳定性测试执行命令
|
|
|
|
|
+
|
|
|
|
|
+**方法 1: Bash 循环(推荐)**
|
|
|
|
|
+```bash
|
|
|
|
|
+# 在 web 目录下执行 10 次测试
|
|
|
|
|
+cd web
|
|
|
|
|
+for i in {1..10}; do
|
|
|
|
|
+ echo "=== 运行 #$i ==="
|
|
|
|
|
+ pnpm test:e2e:chromium file-upload-validation || echo "运行 #$i 失败"
|
|
|
|
|
+done
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**方法 2: 使用脚本**
|
|
|
|
|
+```bash
|
|
|
|
|
+# 创建稳定性测试脚本
|
|
|
|
|
+cat > run-stability-test.sh << 'EOF'
|
|
|
|
|
+#!/bin/bash
|
|
|
|
|
+PASSED=0
|
|
|
|
|
+FAILED=0
|
|
|
|
|
+TIMES=()
|
|
|
|
|
+
|
|
|
|
|
+for i in {1..10}; do
|
|
|
|
|
+ echo "=== 运行 #$i ==="
|
|
|
|
|
+ START=$(date +%s)
|
|
|
|
|
+
|
|
|
|
|
+ if pnpm test:e2e:chromium file-upload-validation; then
|
|
|
|
|
+ PASSED=$((PASSED + 1))
|
|
|
|
|
+ echo "✅ 运行 #$i 通过"
|
|
|
|
|
+ else
|
|
|
|
|
+ FAILED=$((FAILED + 1))
|
|
|
|
|
+ echo "❌ 运行 #$i 失败"
|
|
|
|
|
+ fi
|
|
|
|
|
+
|
|
|
|
|
+ END=$(date +%s)
|
|
|
|
|
+ DURATION=$((END - START))
|
|
|
|
|
+ TIMES+=($DURATION)
|
|
|
|
|
+ echo "⏱️ 耗时: ${DURATION}s"
|
|
|
|
|
+ echo ""
|
|
|
|
|
+done
|
|
|
|
|
+
|
|
|
|
|
+echo "=== 稳定性测试结果 ==="
|
|
|
|
|
+echo "通过: $PASSED/10"
|
|
|
|
|
+echo "失败: $FAILED/10"
|
|
|
|
|
+echo "平均时间: $(awk '{sum+=$1} END {print sum/NR}' <<< "${TIMES[@]}")s"
|
|
|
|
|
+EOF
|
|
|
|
|
+
|
|
|
|
|
+chmod +x run-stability-test.sh
|
|
|
|
|
+./run-stability-test.sh
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**方法 3: 单个场景循环(更精确)**
|
|
|
|
|
+```bash
|
|
|
|
|
+# 只运行特定场景 10 次
|
|
|
|
|
+cd web
|
|
|
|
|
+for i in {1..10}; do
|
|
|
|
|
+ echo "=== 场景 1 运行 #$i ==="
|
|
|
|
|
+ pnpm test:e2e:chromium file-upload-validation --grep "应该成功上传单张照片"
|
|
|
|
|
+done
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 成功标准
|
|
|
|
|
+
|
|
|
|
|
+**Epic 3 完成标准:**
|
|
|
|
|
+- ✅ Story 3.1: 工具函数实现完成
|
|
|
|
|
+- ✅ Story 3.2: 单元测试 ≥80%(实际 91.66%)
|
|
|
|
|
+- ✅ Story 3.3: E2E 集成测试通过
|
|
|
|
|
+- ✅ Story 3.4: 所有工具 bug 已修复
|
|
|
|
|
+- 🔄 Story 3.5: **本 Story - 10次连续运行 100% 通过**
|
|
|
|
|
+
|
|
|
|
|
+**如果达到 100% 通过率:**
|
|
|
|
|
+- Epic 3 可以标记为完成
|
|
|
|
|
+- 可以进入 Epic 4(表单工具开发与验证)
|
|
|
|
|
+
|
|
|
|
|
+**如果 < 100% 通过率:**
|
|
|
|
|
+- 分析失败原因
|
|
|
|
|
+- 修复问题后重新验证
|
|
|
|
|
+- 直达到 100% 通过率
|
|
|
|
|
+
|
|
|
|
|
+### 项目结构说明
|
|
|
|
|
+
|
|
|
|
|
+**相关文件:**
|
|
|
|
|
+```
|
|
|
|
|
+packages/e2e-test-utils/
|
|
|
|
|
+├── src/
|
|
|
|
|
+│ └── file-upload.ts # 文件上传工具(无已知 bug)
|
|
|
|
|
+├── tests/
|
|
|
|
|
+│ └── unit/
|
|
|
|
|
+│ └── file-upload.test.ts # 单元测试(36/36 通过)
|
|
|
|
|
+
|
|
|
|
|
+web/tests/e2e/
|
|
|
|
|
+├── specs/admin/
|
|
|
|
|
+│ └── file-upload-validation.spec.ts # 稳定性测试目标文件
|
|
|
|
|
+├── fixtures/images/
|
|
|
|
|
+│ ├── sample-id-card.jpg
|
|
|
|
|
+│ └── sample-disability-card.jpg
|
|
|
|
|
+└── pages/admin/
|
|
|
|
|
+ └── disability-person.page.ts # Page Object
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 参考文档
|
|
|
|
|
+
|
|
|
|
|
+**架构文档:**
|
|
|
|
|
+- `_bmad-output/planning-artifacts/architecture.md` - 测试策略、稳定性标准
|
|
|
|
|
+- `docs/standards/e2e-radix-testing.md` - E2E 测试标准
|
|
|
|
|
+
|
|
|
|
|
+**Epic 3 相关文档:**
|
|
|
|
|
+- `_bmad-output/planning-artifacts/epics.md` - Epic 3 完整需求
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/3-1-file-upload-tool.md` - 工具实现
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/3-2-upload-unit-tests.md` - 单元测试
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/3-3-upload-e2e-integration.md` - E2E 集成测试
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/3-4-collect-feedback-fix.md` - 反馈和修复
|
|
|
|
|
+
|
|
|
|
|
+**Epic 2 稳定性验证参考:**
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/epic-2-retrospective.md` - 稳定性验证经验
|
|
|
|
|
+
|
|
|
|
|
+### Project Structure Notes
|
|
|
|
|
+
|
|
|
|
|
+**Monorepo 结构对齐:**
|
|
|
|
|
+- E2E 测试在 `web/tests/e2e/` 目录
|
|
|
|
|
+- 使用 `@d8d/e2e-test-utils` workspace 包
|
|
|
|
|
+- 测试 fixtures 位于 `web/tests/fixtures/`
|
|
|
|
|
+
|
|
|
|
|
+**文件组织:**
|
|
|
|
|
+- 测试文件按功能组织在 `specs/admin/` 目录
|
|
|
|
|
+- 使用 `.spec.ts` 后缀命名(Playwright 约定)
|
|
|
|
|
+- 稳定性测试使用现有的 `file-upload-validation.spec.ts`
|
|
|
|
|
+
|
|
|
|
|
+### References
|
|
|
|
|
+
|
|
|
|
|
+**源文档引用:**
|
|
|
|
|
+- [Source: _bmad-output/planning-artifacts/epics.md#Epic-3-Story-3.5] - Story 需求
|
|
|
|
|
+- [Source: _bmad-output/implementation-artifacts/3-4-collect-feedback-fix.md] - 前置 Story 修复
|
|
|
|
|
+
|
|
|
|
|
+**Epic 2 稳定性验证经验:**
|
|
|
|
|
+- [Source: _bmad-output/implementation-artifacts/epic-2-retrospective.md] - 10次连续运行标准
|
|
|
|
|
+
|
|
|
|
|
+**测试文件引用:**
|
|
|
|
|
+- [Source: web/tests/e2e/specs/admin/file-upload-validation.spec.ts] - 稳定性测试目标
|
|
|
|
|
+- [Source: packages/e2e-test-utils/src/file-upload.ts] - 工具实现
|
|
|
|
|
+
|
|
|
|
|
+## Dev Agent Record
|
|
|
|
|
+
|
|
|
|
|
+### Agent Model Used
|
|
|
|
|
+
|
|
|
|
|
+claude-opus-4-5-20251101
|
|
|
|
|
+
|
|
|
|
|
+### Debug Log References
|
|
|
|
|
+
|
|
|
|
|
+### Completion Notes List
|
|
|
|
|
+
|
|
|
|
|
+_待完成:稳定性测试执行和结果记录_
|
|
|
|
|
+
|
|
|
|
|
+### File List
|
|
|
|
|
+
|
|
|
|
|
+_待创建/修改的文件:_
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**Story 创建日期:** 2026-01-10
|
|
|
|
|
+**Story 状态:** ready-for-dev
|