Explorar o código

docs(story): 创建 Story 11.9 - 配置数据验证

- 创建 Story 11.9: 验证订单可以选择平台和公司
- 这是 Epic 11 的最后一个 Story
- 更新 sprint-status.yaml 状态为 ready-for-dev
- 修复 OrderManagementPage 工作状态 UI 选项映射
  - UI 选项: 未入职、已入职、工作中、已离职
  - 原标签: 未就业、待就业、已就业、已离职

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname hai 5 días
pai
achega
393aeafb4e

+ 625 - 0
_bmad-output/implementation-artifacts/11-9-config-data-validation.md

@@ -0,0 +1,625 @@
+# Story 11.9: 配置数据验证(订单可以选择平台和公司)
+
+Status: ready-for-dev
+
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
+
+## Story
+
+作为测试开发者,
+我想要验证订单可以选择平台和公司配置数据,
+以便确保 Epic 11 的配置管理测试为后续的订单管理测试提供稳定的测试数据基础。
+
+## Acceptance Criteria
+
+1. **AC1: 创建测试文件**
+   - 文件路径: `web/tests/e2e/specs/admin/order-config-validation.spec.ts`
+   - 使用 Playwright test 框架
+   - 使用现有的 `OrderManagementPage` Page Object
+   - 定义测试夹具(fixtures)包含 Page Object
+
+2. **AC2: 验证订单可以选择平台**
+   - 首先创建测试用的平台数据
+   - 打开创建订单对话框
+   - 使用 `selectRadixOptionAsync` 选择平台
+   - 验证平台选项正确显示
+   - 验证选择成功
+
+3. **AC3: 验证订单可以选择公司**
+   - 首先创建测试用的平台和公司数据
+   - 打开创建订单对话框
+   - 使用 `selectRadixOptionAsync` 选择公司
+   - 验证公司选项正确显示(可能需要先选择平台)
+   - 验证选择成功
+
+4. **AC4: 验证平台和公司的关联关系**
+   - 创建测试平台和关联的公司
+   - 验证选择平台后,公司选项包含该平台下的公司
+   - 验证不选择平台时也能选择公司(如适用)
+
+5. **AC5: 验证配置数据在订单列表中正确显示**
+   - 创建包含平台和公司的订单
+   - 验证订单列表正确显示平台和公司信息
+   - 验证订单详情正确显示配置数据
+
+6. **AC6: 测试数据清理**
+   - 每个测试用例结束后清理创建的测试数据
+   - 先删除订单,再删除公司,最后删除平台(遵循外键依赖顺序)
+   - 验证清理成功
+
+7. **AC7: 代码质量标准**
+   - TypeScript 类型检查通过(无类型错误)
+   - 测试用例有清晰的描述
+
+## Tasks / Subtasks
+
+- [ ] 任务 1: 创建测试文件和基础结构 (AC: 1, 7)
+  - [ ] 创建文件 `web/tests/e2e/specs/admin/order-config-validation.spec.ts`
+  - [ ] 导入 Playwright test 和 OrderManagementPage
+  - [ ] 导入 PlatformManagementPage 和 CompanyManagementPage(用于创建测试数据)
+  - [ ] 定义测试夹具(adminLoginPage, orderManagementPage, platformManagementPage, companyManagementPage)
+  - [ ] 设置测试基础配置
+
+- [ ] 任务 2: 实现测试前置条件 (AC: 1, 2)
+  - [ ] 实现 `test.beforeEach()` 登录后台
+  - [ ] 导航到订单管理页面
+  - [ ] 验证页面加载完成
+
+- [ ] 任务 3: 实现订单选择平台验证 (AC: 2, 6)
+  - [ ] 编写测试用例:应该成功选择平台
+  - [ ] 使用 PlatformManagementPage 创建测试平台
+  - [ ] 打开创建订单对话框
+  - [ ] 使用 `selectRadixOptionAsync` 选择平台
+  - [ ] 验证选择成功
+  - [ ] 清理测试数据
+
+- [ ] 任务 4: 实现订单选择公司验证 (AC: 3, 6)
+  - [ ] 编写测试用例:应该成功选择公司
+  - [ ] 使用 CompanyManagementPage 创建测试公司(需要先有平台)
+  - [ ] 打开创建订单对话框
+  - [ ] 使用 `selectRadixOptionAsync` 选择公司
+  - [ ] 验证选择成功
+  - [ ] 清理测试数据(订单→公司→平台)
+
+- [ ] 任务 5: 实现平台公司关联关系验证 (AC: 4, 6)
+  - [ ] 编写测试用例:应该验证平台与公司的关联
+  - [ ] 创建平台和多个关联公司
+  - [ ] 验证公司选项包含正确的公司
+  - [ ] 清理测试数据
+
+- [ ] 任务 6: 实现订单列表配置数据显示验证 (AC: 5, 6)
+  - [ ] 编写测试用例:订单列表应正确显示平台和公司
+  - [ ] 创建包含平台和公司的测试订单
+  - [ ] 验证订单列表显示正确的平台和公司信息
+  - [ ] 验证订单详情显示正确的配置数据
+  - [ ] 清理测试数据
+
+- [ ] 任务 7: 实现测试后清理 (AC: 6)
+  - [ ] 每个测试用例内清理测试数据
+  - [ ] 遵循正确的删除顺序:订单→公司→平台
+  - [ ] 验证清理成功
+
+- [ ] 任务 8: 运行测试并验证 (AC: 2, 3, 4, 5, 6, 7)
+  - [ ] 运行测试: `pnpm test:e2e:chromium order-config-validation`
+  - [ ] TypeScript 类型检查通过
+  - [ ] 修复发现的问题
+
+## Dev Notes
+
+### Epic 11 背景和目标
+
+**Epic 11: 基础配置管理测试 (Epic F)**
+
+为平台、公司、渠道配置管理编写 E2E 测试,为后续用户管理和跨端测试提供必要的测试数据。
+
+**实体关系链:**
+```
+Platform (平台)
+  ↓ 1:N
+Company (公司) - 必须 platformId
+  ↓ 1:N
+Order (订单) - 必须 companyId
+  ↓
+Channel (渠道) - 订单的可选条件
+```
+
+**Story 11.9 在 Epic 中的位置:**
+- 这是 Epic 11 的最后一个 Story
+- 验证前面创建的配置管理功能(Platform、Company)可以被订单正确使用
+- 为后续的订单管理测试(Epic 10)提供配置数据验证
+
+### 架构模式和约束
+
+**测试文件结构参考:**
+
+参考 Story 11.2(平台创建测试)和 Story 11.5(公司创建测试)的结构模式:
+- `web/tests/e2e/specs/admin/order-config-validation.spec.ts`(当前)
+
+**标准测试文件结构:**
+```typescript
+import { test, expect } from '@playwright/test';
+import { OrderManagementPage } from '../../pages/admin/order-management.page';
+import { PlatformManagementPage } from '../../pages/admin/platform-management.page';
+import { CompanyManagementPage } from '../../pages/admin/company-management.page';
+
+test.describe('订单配置数据验证', () => {
+  test.beforeEach(async ({ adminLoginPage, orderManagementPage }) => {
+    // 登录后台
+    await adminLoginPage.goto();
+    await adminLoginPage.login('admin', 'admin123');
+
+    // 导航到订单管理页面
+    await orderManagementPage.goto();
+  });
+
+  test.afterEach(async ({ platformManagementPage, companyManagementPage, orderManagementPage }) => {
+    // 清理测试数据:订单→公司→平台
+  });
+
+  test('应该成功选择平台', async ({ orderManagementPage, platformManagementPage }) => {
+    // 测试逻辑
+  });
+});
+```
+
+**测试夹具配置:**
+
+需要在 `web/tests/e2e/utils/test-setup.ts` 确认以下 fixtures 已添加:
+```typescript
+export const test = test.extend<{
+  adminLoginPage: AdminLoginPage;
+  orderManagementPage: OrderManagementPage;
+  platformManagementPage: PlatformManagementPage;
+  companyManagementPage: CompanyManagementPage;
+}>({
+  adminLoginPage: async ({ page }, use) => {
+    await use(new AdminLoginPage(page));
+  },
+  orderManagementPage: async ({ page }, use) => {
+    await use(new OrderManagementPage(page));
+  },
+  platformManagementPage: async ({ page }, use) => {
+    await use(new PlatformManagementPage(page));
+  },
+  companyManagementPage: async ({ page }, use) => {
+    await use(new CompanyManagementPage(page));
+  },
+});
+```
+
+### OrderManagementPage API 参考
+
+**相关方法:**
+
+| 方法 | 描述 | 返回值 |
+|------|------|--------|
+| `goto()` | 导航到订单管理页面 | `Promise<void>` |
+| `openCreateDialog()` | 打开创建订单对话框 | `Promise<void>` |
+| `fillOrderForm(data)` | 填写订单表单 | `Promise<void>` |
+| `createOrder(data)` | 创建订单(完整流程) | `Promise<FormSubmitResult>` |
+| `orderExists(name)` | 验证订单是否存在 | `Promise<boolean>` |
+| `deleteOrder(name)` | 删除订单 | `Promise<boolean>` |
+
+**数据接口:**
+```typescript
+interface OrderData {
+  name: string;                  // 订单名称(必填)
+  expectedStartDate?: string;     // 预计开始日期
+  platformName?: string;          // 平台名称
+  companyName?: string;           // 公司名称
+  channelName?: string;           // 渠道名称
+  status?: OrderStatus;           // 订单状态
+  workStatus?: WorkStatus;        // 工作状态
+}
+```
+
+### 创建订单表单分析
+
+**订单创建对话框字段(来自 OrderManagement.tsx):**
+
+1. **name** - Input(必填)
+   - 占位符: "订单名称" 或类似
+   - 需要确认实际的 data-testid
+
+2. **platformId** - PlatformSelector(异步)
+   - 使用 Radix UI Select 组件
+   - 异步加载平台选项
+   - 标签: "平台"
+   - 需要使用 `selectRadixOptionAsync` 选择
+
+3. **companyId** - CompanySelector(异步)
+   - 使用 Radix UI Select 组件
+   - 异步加载公司选项
+   - 标签: "公司"
+   - 需要使用 `selectRadixOptionAsync` 选择
+   - 可能需要先选择平台(取决于业务逻辑)
+
+4. **expectedStartDate** - DateInput(可选)
+   - 标签: "预计开始日期" 或 "开始日期"
+
+**按钮:**
+- 取消按钮
+- 提交按钮: "创建" 或类似
+
+### 测试用例设计
+
+**测试用例 1: 验证订单可以选择平台**
+```typescript
+test('应该成功选择平台', async ({ orderManagementPage, platformManagementPage }) => {
+  const timestamp = Date.now();
+
+  // 创建测试平台
+  const platformName = `测试平台_${timestamp}`;
+  await platformManagementPage.createPlatform({
+    platformName,
+    contactPerson: '测试联系人',
+    contactPhone: '13800138000'
+  });
+
+  // 打开创建订单对话框
+  await orderManagementPage.openCreateDialog();
+
+  // 选择平台
+  await selectRadixOptionAsync(orderManagementPage.page, '平台', platformName);
+
+  // 填写订单名称
+  await orderManagementPage.page.getByLabel(/订单名称|名称/).fill(`测试订单_${timestamp}`);
+
+  // 提交表单
+  const result = await orderManagementPage.submitForm();
+  expect(result.success).toBe(true);
+
+  // 清理
+  await orderManagementPage.waitForDialogClosed();
+  await orderManagementPage.deleteOrder(`测试订单_${timestamp}`);
+  await platformManagementPage.deletePlatform(platformName);
+});
+```
+
+**测试用例 2: 验证订单可以选择公司**
+```typescript
+test('应该成功选择公司', async ({ orderManagementPage, platformManagementPage, companyManagementPage }) => {
+  const timestamp = Date.now();
+
+  // 创建测试平台
+  const platformName = `测试平台_${timestamp}`;
+  await platformManagementPage.createPlatform({
+    platformName
+  });
+
+  // 创建测试公司
+  const companyName = `测试公司_${timestamp}`;
+  await companyManagementPage.createCompany({
+    companyName
+  }, platformName);
+
+  // 打开创建订单对话框
+  await orderManagementPage.openCreateDialog();
+
+  // 选择公司
+  await selectRadixOptionAsync(orderManagementPage.page, '公司', companyName);
+
+  // 填写订单名称
+  await orderManagementPage.page.getByLabel(/订单名称|名称/).fill(`测试订单_${timestamp}`);
+
+  // 提交表单
+  const result = await orderManagementPage.submitForm();
+  expect(result.success).toBe(true);
+
+  // 清理
+  await orderManagementPage.waitForDialogClosed();
+  await orderManagementPage.deleteOrder(`测试订单_${timestamp}`);
+  await companyManagementPage.deleteCompany(companyName);
+  await platformManagementPage.deletePlatform(platformName);
+});
+```
+
+**测试用例 3: 验证平台和公司关联关系**
+```typescript
+test('应该验证平台与公司的关联', async ({ orderManagementPage, platformManagementPage, companyManagementPage }) => {
+  const timestamp = Date.now();
+
+  // 创建测试平台
+  const platformName = `测试平台_${timestamp}`;
+  await platformManagementPage.createPlatform({ platformName });
+
+  // 创建多个关联公司
+  const company1Name = `测试公司1_${timestamp}`;
+  const company2Name = `测试公司2_${timestamp}`;
+  await companyManagementPage.createCompany({ companyName: company1Name }, platformName);
+  await companyManagementPage.createCompany({ companyName: company2Name }, platformName);
+
+  // 打开创建订单对话框
+  await orderManagementPage.openCreateDialog();
+
+  // 验证公司选项包含创建的公司
+  // (需要根据实际 UI 实现调整验证逻辑)
+
+  // 清理
+  await orderManagementPage.cancelDialog();
+  await companyManagementPage.deleteCompany(company1Name);
+  await companyManagementPage.deleteCompany(company2Name);
+  await platformManagementPage.deletePlatform(platformName);
+});
+```
+
+**测试用例 4: 验证订单列表显示配置数据**
+```typescript
+test('订单列表应正确显示平台和公司', async ({ orderManagementPage, platformManagementPage, companyManagementPage }) => {
+  const timestamp = Date.now();
+
+  // 创建测试配置数据
+  const platformName = `测试平台_${timestamp}`;
+  const companyName = `测试公司_${timestamp}`;
+  const orderName = `测试订单_${timestamp}`;
+
+  await platformManagementPage.createPlatform({ platformName });
+  await companyManagementPage.createCompany({ companyName }, platformName);
+
+  // 创建包含配置数据的订单
+  await orderManagementPage.createOrder({
+    name: orderName,
+    platformName,
+    companyName
+  });
+
+  // 验证订单列表显示正确的平台和公司信息
+  const orderExists = await orderManagementPage.orderExists(orderName);
+  expect(orderExists).toBe(true);
+
+  // 验证订单详情显示配置数据
+  await orderManagementPage.openDetailDialog(orderName);
+  const detailInfo = await orderManagementPage.getOrderDetailInfo();
+  expect(detailInfo.platform).toBe(platformName);
+  expect(detailInfo.company).toBe(companyName);
+
+  // 清理
+  await orderManagementPage.closeDetailDialog();
+  await orderManagementPage.deleteOrder(orderName);
+  await companyManagementPage.deleteCompany(companyName);
+  await platformManagementPage.deletePlatform(platformName);
+});
+```
+
+### 数据清理策略
+
+**遵循外键依赖顺序:**
+```
+订单 → 公司 → 平台
+```
+
+**原因:**
+- 订单依赖公司(companyId 外键)
+- 公司依赖平台(platformId 外键)
+- 必须先删除子级,再删除父级
+
+**清理示例:**
+```typescript
+test.afterEach(async ({ orderManagementPage, companyManagementPage, platformManagementPage }) => {
+  // 按相反顺序清理
+  if (orderName) {
+    await orderManagementPage.deleteOrder(orderName);
+  }
+  if (companyName) {
+    await companyManagementPage.deleteCompany(companyName);
+  }
+  if (platformName) {
+    await platformManagementPage.deletePlatform(platformName);
+  }
+});
+```
+
+### PlatformSelector 和 CompanySelector 集成要点
+
+**使用 `@d8d/e2e-test-utils` 的 `selectRadixOptionAsync`:**
+```typescript
+import { selectRadixOptionAsync } from '@d8d/e2e-test-utils';
+
+// 选择平台
+await selectRadixOptionAsync(orderManagementPage.page, '平台', platformName);
+
+// 选择公司
+await selectRadixOptionAsync(orderManagementPage.page, '公司', companyName);
+```
+
+**异步加载等待:**
+- 平台和公司选项都是异步加载的
+- 使用 `selectRadixOptionAsync` 确保选项加载完成
+- 默认超时 5000ms,可以根据需要调整
+
+### 项目结构约束
+
+**测试文件存放路径:**
+```
+web/tests/e2e/
+├── pages/admin/
+│   ├── order-management.page.ts      # 订单管理 Page Object(已存在)
+│   ├── platform-management.page.ts   # 平台管理 Page Object(已存在)
+│   └── company-management.page.ts    # 公司管理 Page Object(已存在)
+├── specs/admin/
+│   └── order-config-validation.spec.ts # 订单配置验证测试(当前)
+└── utils/
+    └── test-setup.ts                  # 测试夹具配置
+```
+
+**依赖文件:**
+- `web/tests/e2e/pages/admin/order-management.page.ts` - 已存在
+- `web/tests/e2e/pages/admin/platform-management.page.ts` - Story 11.1 创建
+- `web/tests/e2e/pages/admin/company-management.page.ts` - Story 11.4 创建
+- `web/tests/e2e/utils/test-setup.ts` - 需要确认包含所有必要的 fixtures
+
+### 测试运行命令
+
+**运行单个测试文件:**
+```bash
+cd web
+pnpm test:e2e:chromium order-config-validation.spec.ts
+```
+
+**运行单个测试用例:**
+```bash
+cd web
+pnpm test:e2e:chromium order-config-validation.spec.ts -g "应该成功选择平台"
+```
+
+**快速失败模式(调试):**
+```bash
+cd web
+timeout 60 pnpm test:e2e:chromium order-config-validation.spec.ts
+```
+
+### 依赖关系
+
+**Epic 11 内部依赖:**
+- Story 11.1: ✅ 已完成(PlatformManagementPage)
+- Story 11.2: ✅ 已完成(平台创建测试)
+- Story 11.4: ✅ 已完成(CompanyManagementPage)
+- Story 11.5: ✅ 已完成(公司创建测试)
+- Story 11.9: 配置数据验证(当前)
+
+**外部依赖:**
+- Epic 1, 2: `@d8d/e2e-test-utils` 包(已存在)
+- `web/tests/e2e/pages/admin/order-management.page.ts`: 已存在
+- `web/tests/e2e/utils/test-setup.ts`: 需要包含所有必要的 fixtures
+
+### 测试标准和规范
+
+遵循项目测试标准:
+- `docs/standards/testing-standards.md`
+- `docs/standards/web-ui-testing-standards.md`
+
+**关键测试原则:**
+1. 测试独立性:每个测试用例独立运行
+2. 数据清理:每个测试结束后清理自己创建的数据
+3. 清晰断言:使用 expect() 明确断言预期结果
+4. 等待策略:使用 Playwright 的 auto-waiting
+
+### 前序 Story (11.1-11.8) 关键经验
+
+从 Story 11.1-11.8 中学到的关键经验:
+
+1. **异步 Select 处理:**
+   - PlatformSelector 和 CompanySelector 都是异步加载的
+   - 必须使用 `selectRadixOptionAsync`
+   - 设置合理的超时时间(默认 5000ms)
+
+2. **测试数据要求:**
+   - 后端 Zod schema 要求字段类型正确
+   - 空字符串在某些字段中是有效值(有默认值)
+   - 测试不填的可选字段应该设为 undefined
+
+3. **页面刷新:**
+   - 创建/删除数据后需要刷新页面或重新导航
+   - 使用 `page.reload()` 或 `goto()` 刷新
+
+4. **选择器优先级:**
+   - 优先使用 data-testid(最稳定)
+   - 其次使用 role + label(较稳定)
+   - 避免使用文本选择器(可能变化)
+
+5. **数据清理顺序:**
+   - 必须遵循外键依赖顺序:子级→父级
+   - 订单→公司→平台
+   - 验证删除成功后再结束测试
+
+6. **API 删除策略(来自 Story 11.2):**
+   - 使用 API 直接删除,绕过 UI 的不可靠性
+   - 删除成功后刷新页面确保列表更新
+
+### 已知问题和注意事项
+
+1. **OrderManagementPage 需要验证:**
+   - 确认 `fillOrderForm` 方法中平台和公司的选择逻辑正确
+   - 确认表单字段的数据-testid 或 label 是否正确
+
+2. **公司选择可能需要先选择平台:**
+   - 根据业务逻辑,CompanySelector 可能需要先选择平台
+   - 需要在测试中验证这个行为
+
+3. **测试数据唯一性:**
+   - 使用时间戳确保唯一性
+   - 避免不同测试之间的数据冲突
+
+4. **配置数据在列表中的显示:**
+   - 需要验证订单列表中平台和公司列的显示
+   - 可能需要滚动列表才能看到某些列
+
+### Epic 11 完成标准
+
+**Epic 11 回顾:**
+- ✅ Story 11.1: Platform Page Object
+- ✅ Story 11.2: 创建测试平台
+- ✅ Story 11.3: 验证平台列表显示
+- ✅ Story 11.4: Company Page Object
+- ✅ Story 11.5: 创建测试公司
+- ✅ Story 11.6: 验证公司列表显示
+- ✅ Story 11.7: Channel Page Object
+- ✅ Story 11.8: 创建测试渠道
+- 🔄 Story 11.9: 配置数据验证(当前)
+
+**Epic 11 完成条件:**
+1. 所有 Platform、Company、Channel 的 CRUD 测试完成
+2. 验证订单可以使用配置数据(平台和公司)
+3. 为后续 Epic(订单管理、用户管理)提供稳定的测试数据
+
+### References
+
+- [Epic 11 基础配置管理测试](../planning-artifacts/epics.md#epic-11-基础配置管理测试-epic-f)
+- [Story 11.2 平台创建测试](./11-2-platform-create-test.story.md) - 参考测试模式
+- [Story 11.5 公司创建测试](./11-5-company-create-test.story.md) - 参考测试模式
+- [OrderManagementPage](../../web/tests/e2e/pages/admin/order-management.page.ts) - 使用此 Page Object
+- [PlatformManagementPage](../../web/tests/e2e/pages/admin/platform-management.page.ts) - 用于创建测试平台
+- [CompanyManagementPage](../../web/tests/e2e/pages/admin/company-management.page.ts) - 用于创建测试公司
+- [测试标准](../../docs/standards/testing-standards.md)
+- [Web UI 测试标准](../../docs/standards/web-ui-testing-standards.md)
+
+## Dev Agent Record
+
+### Agent Model Used
+
+Claude (d8d-model)
+
+### Debug Log References
+
+无特殊问题需要记录。Story 创建过程顺利。
+
+### Completion Notes List
+
+**Story 创建完成:**
+1. ✅ 分析 Story 11.9 需求:验证订单可以选择平台和公司
+2. ✅ 创建完整的 Story 文档
+3. ✅ 包含所有验收标准和任务分解
+4. ✅ 提供详细的 Dev Notes 指导开发者
+5. ✅ 参考 Story 11.2 和 11.5 的测试模式
+6. ✅ 分析 OrderManagementPage 的 API 和方法
+7. ✅ 提供完整的测试用例示例
+8. ✅ 说明数据清理策略(订单→公司→平台)
+9. ✅ 强调异步 Select 的处理方式
+10. ✅ 设置状态为 ready-for-dev
+
+**文档包含内容:**
+- Epic 11 背景和目标
+- 架构模式和约束
+- OrderManagementPage API 参考
+- 创建订单表单分析
+- 完整的测试用例设计
+- 数据清理策略
+- PlatformSelector 和 CompanySelector 集成要点
+- 项目结构约束
+- 测试标准和规范
+- 前序 Story 关键经验
+- Epic 11 完成标准
+
+### File List
+
+**新增文件:**
+- `_bmad-output/implementation-artifacts/11-9-config-data-validation.md` - 本 story 文件
+
+**依赖的已有文件:**
+- `web/tests/e2e/pages/admin/order-management.page.ts` - 订单管理 Page Object(已存在)
+- `web/tests/e2e/pages/admin/platform-management.page.ts` - Story 11.1 创建
+- `web/tests/e2e/pages/admin/company-management.page.ts` - Story 11.4 创建
+- `web/tests/e2e/utils/test-setup.ts` - 测试夹具配置
+
+**将要创建的文件:**
+- `web/tests/e2e/specs/admin/order-config-validation.spec.ts` - 订单配置验证 E2E 测试

+ 1 - 1
_bmad-output/implementation-artifacts/sprint-status.yaml

@@ -178,7 +178,7 @@ development_status:
   11-6-company-list-test: done         # 验证公司列表显示 ✅ 14 个测试全部通过,代码审查完成,所有 HIGH 和 MEDIUM 问题已修复 (2026-01-12)
   11-7-channel-page-object: done           # Channel 管理 Page Object(可选)
   11-8-channel-create-test: done           # 创建测试渠道(可选) - ✅ 11/11 测试通过 (2.9m) (2026-01-13)
-  11-9-config-validation-test: backlog     # 验证订单可以选择平台和公司
+  11-9-config-validation-test: ready-for-dev     # 验证订单可以选择平台和公司 - Story 文件已创建 (2026-01-13)
   epic-11-retrospective: optional
 
   # Epic 12: 用户管理与小程序登录测试 (Epic D - 业务测试 Epic)

+ 76 - 16
web/tests/e2e/pages/admin/order-management.page.ts

@@ -922,34 +922,94 @@ export class OrderManagementPage {
   async updatePersonWorkStatus(personName: string, newStatus: WorkStatus) {
     const dialog = this.page.locator('[role="dialog"]');
 
-    // 找到所有表格,选择第二个表格(绑定人员列表)
-    // 第一个表格是待添加人员列表,第二个是绑定人员列表
-    const allTables = await dialog.locator('table').all();
-    const boundPersonsTable = allTables[1]; // 第二个表格
+    // 等待对话框完全加载
+    await dialog.waitFor({ state: 'visible', timeout: 5000 });
+
+    // 从 error-context.md 可知:
+    // 1. 对话框中有"绑定人员列表"表格
+    // 2. 表格列:ID 姓名 性别 残疾类型 联系电话 入职日期 离职日期 工作状态 薪资
+    // 3. 工作状态列直接是 combobox,不需要点击编辑按钮
+
+    // 查找所有表格
+    const allTables = dialog.locator('table');
+    const allTableCount = await allTables.count();
+    console.debug(`对话框中总共有 ${allTableCount} 个表格`);
+
+    let personTable = allTables.first();
+    // 找到包含"绑定人员"或"工作状态"列的表格(第二个表格是绑定人员列表)
+    for (let i = 0; i < allTableCount; i++) {
+      const table = allTables.nth(i);
+      const tableText = await table.textContent();
+      if (tableText && (tableText.includes('绑定人员') || tableText.includes('工作状态'))) {
+        personTable = table;
+        console.debug(`找到人员表格(索引 ${i})`);
+        break;
+      }
+    }
+
+    // 在表格中查找包含指定人员名称的行
+    const targetRow = personTable.locator('tbody tr').filter({ hasText: personName }).first();
+    const rowCount = await targetRow.count();
+    console.debug(`找到 ${rowCount} 个匹配的人员行`);
 
-    // 在该表格中找到包含人员姓名的行
-    const allRows = boundPersonsTable.locator('tbody tr');
-    const targetRow = allRows.filter({ hasText: personName });
+    if (rowCount === 0) {
+      throw new Error(`未找到人员 ${personName}`);
+    }
+
+    // 等待行可见
+    await targetRow.waitFor({ state: 'visible', timeout: 5000 });
+
+    // 从 error-context.md 可知,工作状态在单元格中是一个 combobox
+    // 表格列:ID 姓名 性别 残疾类型 联系电话 入职日期 离职日期 工作状态 薪资
+    // 工作状态是倒数第二列(薪资是最后一列)
 
-    // 工作状态在倒数第二列(最后一列是薪资)
     const cells = targetRow.locator('td');
     const cellCount = await cells.count();
+    console.debug(`人员行有 ${cellCount} 个单元格`);
 
-    // 工作状态是倒数第二列(索引 cellCount - 2)
+    // 工作状态在倒数第二列
     const workStatusCell = cells.nth(cellCount - 2);
-    const workStatusSelect = workStatusCell.locator('[role="combobox"]');
+    const workStatusCombobox = workStatusCell.getByRole('combobox');
+    const comboboxCount = await workStatusCombobox.count();
+    console.debug(`工作状态 combobox 数量: ${comboboxCount}`);
+
+    if (comboboxCount === 0) {
+      throw new Error(`未找到人员 ${personName} 的工作状态选择器`);
+    }
 
-    await workStatusSelect.click();
+    await workStatusCombobox.click({ timeout: 5000 });
+    console.debug('工作状态 combobox 已点击');
 
     // 等待下拉选项显示
-    await this.page.waitForTimeout(300);
+    await this.page.waitForTimeout(500);
 
     // 使用中文标签选择选项
-    const workStatusLabel = WORK_STATUS_LABELS[newStatus];
-    const option = this.page.getByRole('option').filter({ hasText: workStatusLabel });
-    await option.click();
+    // 注意:UI 中的工作状态选项与 WORK_STATUS_LABELS 不同
+    // UI 选项:未入职、已入职、工作中、已离职
+    // WORK_STATUS_LABELS:未就业、待就业、已就业、已离职
+    const statusMapping: Record<WorkStatus, string> = {
+      not_employed: '未入职',
+      pending: '已入职',  // "待就业" 对应 UI 中的 "已入职"
+      employed: '工作中',  // "已就业" 对应 UI 中的 "工作中"
+      resigned: '已离职',
+    };
+    const newWorkStatusLabel = statusMapping[newStatus];
+    console.debug(`尝试选择状态: ${newWorkStatusLabel}`);
 
-    await this.page.waitForLoadState('networkidle');
+    const optionLocator = this.page.getByRole('option', { name: newWorkStatusLabel });
+    const optionCount = await optionLocator.count();
+    console.debug(`找到 ${optionCount} 个选项`);
+
+    if (optionCount === 0) {
+      throw new Error(`未找到工作状态选项: ${newWorkStatusLabel}`);
+    }
+
+    await optionLocator.first().click({ timeout: 5000 });
+    console.debug(`工作状态已更新为: ${newWorkStatusLabel}`);
+
+    // 使用较短的超时时间等待网络空闲
+    await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 })
+      .catch(() => console.debug('domcontentloaded 等待超时,继续'));
     await this.page.waitForTimeout(500);
   }