Bladeren bron

test(e2e): 修复 Story 10.9 订单人员工作状态 E2E 测试

主要修复:
1. 前端 Bug: OrderDetailModal.tsx 中 parseInt() 将枚举字符串转为 NaN
2. 测试页面对象: getPersonListFromDetail() 错误选择"待添加人员列表"表格
3. 测试数据隔离: 使用全局计数器确保每个测试生成唯一身份证号

测试结果: 3/3 工作状态测试通过

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 5 dagen geleden
bovenliggende
commit
eedf7e894a

+ 2 - 0
CLAUDE.md

@@ -14,6 +14,8 @@
 - vitest中,只有console.debug会显示,其他的都屏蔽了
 - vitest中,用import 来配合 vi.mocked,而不是require
 - **E2E测试**:
+  - **推荐使用子代理运行**: 运行 Playwright E2E 测试时,使用 Task 工具的 Bash 子代理方式运行,速度更快且多个 Playwright 进程不会冲突
+    - 示例提示词: "在 web 目录下运行 `pnpm exec playwright test --config=tests/e2e/playwright.config.ts --project=chromium --grep \"测试名称\"`"
   - 运行所有E2E测试: `pnpm test:e2e:chromium`
   - 运行单个测试文件: `pnpm test:e2e:chromium <测试文件名>` (如: `pnpm test:e2e:chromium disability-person-complete`)
   - **快速失败模式** (推荐调试时使用): 使用 Linux `timeout` 命令限制总运行时间

+ 1 - 1
allin-packages/order-management-ui/src/components/OrderDetailModal.tsx

@@ -708,7 +708,7 @@ const OrderDetailModal: React.FC<OrderDetailModalProps> = ({
                                   onValueChange={(value) =>
                                     handleUpdateWorkStatus(
                                       person.personId,
-                                      parseInt(value) as unknown as WorkStatus,
+                                      value as WorkStatus,
                                     )
                                   }
                                   data-testid={`order-detail-work-status-select-${person.personId}`}

+ 29 - 13
web/tests/e2e/pages/admin/order-management.page.ts

@@ -30,9 +30,9 @@ export const ORDER_STATUS_LABELS: Record<OrderStatus, string> = {
  * 工作状态常量
  */
 export const WORK_STATUS = {
-  NOT_EMPLOYED: 'not_employed',
-  PENDING: 'pending',
-  EMPLOYED: 'employed',
+  NOT_WORKING: 'not_working',
+  PRE_WORKING: 'pre_working',
+  WORKING: 'working',
   RESIGNED: 'resigned',
 } as const;
 
@@ -45,9 +45,9 @@ export type WorkStatus = typeof WORK_STATUS[keyof typeof WORK_STATUS];
  * 工作状态显示名称映射
  */
 export const WORK_STATUS_LABELS: Record<WorkStatus, string> = {
-  not_employed: '未就业',
-  pending: '待就业',
-  employed: '已就业',
+  not_working: '未入职',
+  pre_working: '已入职',
+  working: '工作中',
   resigned: '已离职',
 } as const;
 
@@ -665,13 +665,29 @@ export class OrderManagementPage {
     const dialog = this.page.locator('[role="dialog"]');
     const result: Array<{ name?: string; workStatus?: string; hireDate?: string; salary?: string }> = [];
 
-    // 查找人员列表区域(通常在详情对话框中有一个表格或列表展示人员)
-    // 尝试多种可能的定位策略
-    const personTable = dialog.locator('table').filter({ hasText: /人员|员工/ });
+    // 查找所有表格,对话框中可能有两个表格:
+    // 1. "待添加人员列表" - 临时表格,包含未确认的人员
+    // 2. "绑定人员列表" - 实际已绑定到订单的人员
+    // 我们需要第二个"绑定人员列表"表格
+    const allTables = dialog.locator('table');
+    const tableCount = await allTables.count();
+
+    // 查找"绑定人员列表"表格(通常是包含"工作状态"列的表格)
+    let personTable;
+    for (let i = 0; i < tableCount; i++) {
+      const table = allTables.nth(i);
+      const tableText = await table.textContent();
+      // 绑定人员列表表格包含"工作状态"列,而待添加人员列表没有
+      if (tableText && tableText.includes('工作状态')) {
+        personTable = table;
+        break;
+      }
+    }
+
     const personList = dialog.locator('[class*="person"], [class*="employee"], [data-testid*="person"]');
 
     // 优先使用表格形式
-    if (await personTable.count() > 0) {
+    if (personTable) {
       const rows = personTable.locator('tbody tr');
       const rowCount = await rows.count();
 
@@ -988,9 +1004,9 @@ export class OrderManagementPage {
     // UI 选项:未入职、已入职、工作中、已离职
     // WORK_STATUS_LABELS:未就业、待就业、已就业、已离职
     const statusMapping: Record<WorkStatus, string> = {
-      not_employed: '未入职',
-      pending: '已入职',  // "待就业" 对应 UI 中的 "已入职"
-      employed: '工作中',  // "已就业" 对应 UI 中的 "工作中"
+      not_working: '未入职',
+      pre_working: '已入职',
+      working: '工作中',
       resigned: '已离职',
     };
     const newWorkStatusLabel = statusMapping[newStatus];

+ 119 - 18
web/tests/e2e/specs/admin/order-person.spec.ts

@@ -210,12 +210,30 @@ async function selectDisabledPersonInAddDialog(
   return true;
 }
 
+// 全局计数器,确保每个测试生成唯一的数据
+let testDataCounter = 0;
+
 function generateUniqueTestData() {
   const timestamp = Date.now();
+  const counter = ++testDataCounter;
   const random = Math.floor(Math.random() * 10000);
+  // 生成18位身份证号:110101(地区码6位) + 19900101(出生日期8位) + XXX(顺序码3位) + X(校验码1位)
+  // 使用计数器和随机数作为顺序码,确保唯一性
+  const sequenceCode = String(counter).padStart(2, '0') + String(random).slice(0, 1);
+  const idCard = '110101' + '19900101' + sequenceCode + '1'; // 6+8+3+1=18位
   return {
-    orderName: '测试订单_' + timestamp + '_' + random,
-    personName: '测试残疾人_' + timestamp + '_' + random,
+    orderName: '测试订单_' + timestamp + '_' + counter + '_' + random,
+    personName: '测试残疾人_' + timestamp + '_' + counter + '_' + random,
+    // 18位身份证号
+    idCard,
+    phone: '138' + String(counter).padStart(4, '0') + String(random).padStart(4, '0'),
+    gender: '男',
+    disabilityType: '视力残疾',
+    disabilityLevel: '一级',
+    disabilityId: '残疾证' + sequenceCode + String(timestamp).slice(-6),
+    idAddress: '北京市东城区测试地址' + timestamp + '_' + counter,
+    province: '北京市',
+    city: '北京市',
     hireDate: '2025-01-15',
     salary: 5000,
   };
@@ -612,12 +630,31 @@ test.describe('订单人员关联测试', () => {
   });
 
   test.describe('管理工作状态', () => {
-    test('应该能修改人员工作状态:未就业 → 待就业', async ({ orderManagementPage, page }) => {
-      if (!createdPersonName || !createdPlatformName || !createdCompanyName) {
-        test.skip(true, '缺少测试数据(残疾人、平台或公司)');
+    test('应该能修改人员工作状态:未就业 → 待就业', async ({ orderManagementPage, page, request }) => {
+      if (!createdPlatformName || !createdCompanyName) {
+        test.skip(true, '缺少测试数据(平台或公司)');
         return;
       }
+      // 为此测试创建唯一的残疾人数据
       const testData = generateUniqueTestData();
+      const personData = {
+        name: testData.personName,
+        gender: testData.gender,
+        idCard: testData.idCard,
+        disabilityId: testData.disabilityId,
+        disabilityType: testData.disabilityType,
+        disabilityLevel: testData.disabilityLevel,
+        idAddress: testData.idAddress,
+        phone: testData.phone,
+        province: testData.province,
+        city: testData.city,
+      };
+      const createdPerson = await createDisabledPersonViaAPI(request, personData);
+      if (!createdPerson) {
+        test.skip(true, '无法创建残疾人数据');
+        return;
+      }
+      console.debug('已创建残疾人:', createdPerson.name, 'ID:', createdPerson.id);
       await orderManagementPage.openCreateDialog();
       await page.getByLabel(/订单名称|名称/).fill(testData.orderName);
 
@@ -676,7 +713,7 @@ test.describe('订单人员关联测试', () => {
       }
 
       await page.getByLabel(/预计开始日期|开始日期/).fill('2025-01-15');
-      const hasPerson = await selectDisabledPersonInAddDialog(page, createdPersonName);
+      const hasPerson = await selectDisabledPersonInAddDialog(page, createdPerson.name);
       if (!hasPerson) {
         await orderManagementPage.cancelDialog();
         test.skip(true, '没有可用的残疾人数据');
@@ -689,21 +726,66 @@ test.describe('订单人员关联测试', () => {
       // 等待订单详情对话框加载完成
       await page.waitForTimeout(500);
 
-      // 直接使用 createdPersonName,因为我们知道创建订单时添加了这个人员
-      // 跳过 getPersonListFromDetail,直接使用已知的人员名称
-      await orderManagementPage.updatePersonWorkStatus(createdPersonName!, 'pending');
+      // 监听网络响应以捕获 400 错误的详细信息
+      const apiResponses: any[] = [];
+      page.on('response', async (response) => {
+        if (response.status() === 400) {
+          const url = response.url();
+          const contentType = response.headers()['content-type'];
+          if (contentType && contentType.includes('application/json')) {
+            try {
+              const body = await response.json();
+              apiResponses.push({ url, status: response.status(), body });
+              console.debug('API 400 错误详情:', JSON.stringify(body, null, 2));
+            } catch (e) {
+              const text = await response.text();
+              apiResponses.push({ url, status: response.status(), body: text });
+              console.debug('API 400 错误详情:', text);
+            }
+          }
+        }
+      });
+
+      // 获取实际绑定的人员列表,使用第一个人员的名称
+      const personList = await orderManagementPage.getPersonListFromDetail();
+      await orderManagementPage.updatePersonWorkStatus(personList[0].name, 'pre_working');
+
+      // 如果有 400 错误,打印详细信息
+      if (apiResponses.length > 0) {
+        console.debug('捕获到的 API 错误:', JSON.stringify(apiResponses, null, 2));
+      }
+
       const successToast = page.locator('[data-sonner-toast][data-type="success"]');
       const hasSuccess = await successToast.count() > 0;
       expect(hasSuccess).toBe(true);
       await orderManagementPage.closeDetailDialog();
     });
 
-    test('应该能修改人员工作状态:待就业 → 已就业', async ({ orderManagementPage, page }) => {
-      if (!createdPersonName || !createdPlatformName || !createdCompanyName) {
-        test.skip(true, '缺少测试数据(残疾人、平台或公司)');
+    test('应该能修改人员工作状态:待就业 → 已就业', async ({ orderManagementPage, page, request }) => {
+      if (!createdPlatformName || !createdCompanyName) {
+        test.skip(true, '缺少测试数据(平台或公司)');
         return;
       }
+      // 为此测试创建唯一的残疾人数据
       const testData = generateUniqueTestData();
+      const personData = {
+        name: testData.personName,
+        gender: testData.gender,
+        idCard: testData.idCard,
+        disabilityId: testData.disabilityId,
+        disabilityType: testData.disabilityType,
+        disabilityLevel: testData.disabilityLevel,
+        idAddress: testData.idAddress,
+        phone: testData.phone,
+        province: testData.province,
+        city: testData.city,
+      };
+      const createdPerson = await createDisabledPersonViaAPI(request, personData);
+      if (!createdPerson) {
+        test.skip(true, '无法创建残疾人数据');
+        return;
+      }
+      console.debug('已创建残疾人:', createdPerson.name, 'ID:', createdPerson.id);
       await orderManagementPage.openCreateDialog();
       await page.getByLabel(/订单名称|名称/).fill(testData.orderName);
 
@@ -762,7 +844,7 @@ test.describe('订单人员关联测试', () => {
       }
 
       await page.getByLabel(/预计开始日期|开始日期/).fill('2025-01-15');
-      const hasPerson = await selectDisabledPersonInAddDialog(page, createdPersonName);
+      const hasPerson = await selectDisabledPersonInAddDialog(page, createdPerson.name);
       if (!hasPerson) {
         await orderManagementPage.cancelDialog();
         test.skip(true, '没有可用的残疾人数据');
@@ -772,19 +854,38 @@ test.describe('订单人员关联测试', () => {
       await orderManagementPage.waitForDialogClosed();
       await orderManagementPage.openPersonManagementDialog(testData.orderName);
       const personList = await orderManagementPage.getPersonListFromDetail();
-      await orderManagementPage.updatePersonWorkStatus(personList[0].name, 'employed');
+      await orderManagementPage.updatePersonWorkStatus(personList[0].name, 'working');
       const successToast = page.locator('[data-sonner-toast][data-type="success"]');
       const hasSuccess = await successToast.count() > 0;
       expect(hasSuccess).toBe(true);
       await orderManagementPage.closeDetailDialog();
     });
 
-    test('应该能修改人员工作状态:已就业 → 已离职', async ({ orderManagementPage, page }) => {
-      if (!createdPersonName || !createdPlatformName || !createdCompanyName) {
-        test.skip(true, '缺少测试数据(残疾人、平台或公司)');
+    test('应该能修改人员工作状态:已就业 → 已离职', async ({ orderManagementPage, page, request }) => {
+      if (!createdPlatformName || !createdCompanyName) {
+        test.skip(true, '缺少测试数据(平台或公司)');
         return;
       }
+      // 为此测试创建唯一的残疾人数据
       const testData = generateUniqueTestData();
+      const personData = {
+        name: testData.personName,
+        gender: testData.gender,
+        idCard: testData.idCard,
+        disabilityId: testData.disabilityId,
+        disabilityType: testData.disabilityType,
+        disabilityLevel: testData.disabilityLevel,
+        idAddress: testData.idAddress,
+        phone: testData.phone,
+        province: testData.province,
+        city: testData.city,
+      };
+      const createdPerson = await createDisabledPersonViaAPI(request, personData);
+      if (!createdPerson) {
+        test.skip(true, '无法创建残疾人数据');
+        return;
+      }
+      console.debug('已创建残疾人:', createdPerson.name, 'ID:', createdPerson.id);
       await orderManagementPage.openCreateDialog();
       await page.getByLabel(/订单名称|名称/).fill(testData.orderName);
 
@@ -843,7 +944,7 @@ test.describe('订单人员关联测试', () => {
       }
 
       await page.getByLabel(/预计开始日期|开始日期/).fill('2025-01-15');
-      const hasPerson = await selectDisabledPersonInAddDialog(page, createdPersonName);
+      const hasPerson = await selectDisabledPersonInAddDialog(page, createdPerson.name);
       if (!hasPerson) {
         await orderManagementPage.cancelDialog();
         test.skip(true, '没有可用的残疾人数据');