Bläddra i källkod

feat(story-13.3): 完善人才小程序订单相关 Page Object 方法

- 添加 TalentOrderData 和 TalentOrderDetailData 接口定义
- 实现 navigateToMyOrders() - 导航到我的订单页面
- 实现 getMyOrders() - 获取订单列表
- 实现 waitForOrderToAppear() - 轮询等待订单出现
- 实现 openOrderDetail() - 打开订单详情
- 实现 getOrderDetail() - 获取订单详情信息
- 完善人员添加同步测试,使用新的 Page Object 方法
- 优化残疾人选择逻辑,动态选择第一个可用的残疾人
- 精确匹配 Toast 消息,避免误匹配
- 修复 ESLint 错误:使用正确的类型断言,为未使用参数添加下划线前缀

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 3 dagar sedan
förälder
incheckning
43963174e0

+ 304 - 0
web/tests/e2e/pages/mini/talent-mini.page.ts

@@ -13,6 +13,50 @@ const MINI_LOGIN_URL = `${MINI_BASE_URL}/talent-mini`;
 const TOKEN_KEY = 'talent_token';
 const USER_KEY = 'talent_user';
 
+/**
+ * 人才小程序订单数据类型定义 (Story 13.3)
+ */
+
+/**
+ * 订单数据接口
+ */
+export interface TalentOrderData {
+  /** 订单 ID */
+  id: number;
+  /** 订单名称 */
+  name: string;
+  /** 公司名称 */
+  companyName?: string;
+  /** 订单状态 */
+  status?: string;
+  /** 创建时间 */
+  createdAt?: string;
+}
+
+/**
+ * 订单详情数据接口
+ */
+export interface TalentOrderDetailData {
+  /** 订单 ID */
+  id: number;
+  /** 订单名称 */
+  name: string;
+  /** 公司名称 */
+  companyName: string;
+  /** 平台名称 */
+  platformName?: string;
+  /** 订单状态 */
+  status: string;
+  /** 预计人数 */
+  expectedCount?: number;
+  /** 实际人数 */
+  actualCount?: number;
+  /** 预计开始日期 */
+  expectedStartDate?: string;
+  /** 薪资 */
+  salary?: number;
+}
+
 /**
  * 人才小程序 Page Object
  *
@@ -492,4 +536,264 @@ export class TalentMiniPage {
     await expect(this.passwordInputPlaceholder).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
     await expect(this.loginButtonText).toBeVisible({ timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
   }
+
+  // ===== 我的订单方法 (Story 13.3) =====
+
+  /**
+   * 导航到"我的订单"页面 (Story 13.3)
+   *
+   * 人才小程序的"我的订单"页面显示该用户(残疾人)关联的所有订单
+   *
+   * @example
+   * await talentMiniPage.navigateToMyOrders();
+   */
+  async navigateToMyOrders(): Promise<void> {
+    // 点击底部导航的"我的"按钮
+    const myButton = this.page.getByText('我的', { exact: true }).first();
+    await myButton.click();
+
+    // 等待导航完成
+    await this.page.waitForTimeout(TIMEOUTS.SHORT);
+
+    // 点击"我的订单"菜单项
+    const myOrdersText = this.page.getByText('我的订单').first();
+    await myOrdersText.click();
+
+    // 等待订单列表页面加载
+    await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.PAGE_LOAD });
+    await this.page.waitForTimeout(TIMEOUTS.MEDIUM);
+
+    console.debug('[人才小程序] 已导航到我的订单页面');
+  }
+
+  /**
+   * 获取"我的订单"列表 (Story 13.3)
+   *
+   * @returns 订单数据数组
+   * @example
+   * const orders = await talentMiniPage.getMyOrders();
+   * console.debug(`找到 ${orders.length} 个订单`);
+   */
+  async getMyOrders(): Promise<TalentOrderData[]> {
+    const orders: TalentOrderData[] = [];
+
+    // 查找所有订单卡片
+    const orderCards = this.page.locator('.bg-white.p-4, .card, [class*="order-card"]');
+
+    const count = await orderCards.count();
+    console.debug(`[人才小程序] 找到 ${count} 个订单卡片`);
+
+    for (let i = 0; i < count; i++) {
+      const card = orderCards.nth(i);
+      const cardText = await card.textContent();
+
+      if (!cardText) continue;
+
+      const order: TalentOrderData = {
+        id: 0,
+        name: '',
+      };
+
+      // 提取订单名称(通常是加粗的文本)
+      const nameElement = card.locator('.font-semibold, .font-bold, .text-lg').first();
+      const nameCount = await nameElement.count();
+      if (nameCount > 0) {
+        order.name = (await nameElement.textContent())?.trim() || '';
+      } else {
+        // 如果没有找到名称元素,尝试从文本中提取
+        const lines = cardText.split('\n').map(l => l.trim()).filter(l => l);
+        if (lines.length > 0) {
+          order.name = lines[0];
+        }
+      }
+
+      // 提取公司名称
+      const companyMatch = cardText.match(/公司[::]?\s*([^\n]+)/);
+      if (companyMatch) {
+        order.companyName = companyMatch[1].trim();
+      }
+
+      // 提取订单状态
+      const statusKeywords = ['进行中', '已完成', '草稿', '已确认', '未入职', '已入职', '工作中', '已离职'];
+      for (const keyword of statusKeywords) {
+        if (cardText.includes(keyword)) {
+          order.status = keyword;
+          break;
+        }
+      }
+
+      // 提取订单 ID(从 URL 或数据属性中)
+      const cardLink = card.locator('a').or(card);
+      const href = await cardLink.getAttribute('href');
+      if (href) {
+        const idMatch = href.match(/id[=]?(\d+)/);
+        if (idMatch) {
+          order.id = parseInt(idMatch[1], 10);
+        }
+      }
+
+      if (order.name) {
+        orders.push(order);
+      }
+    }
+
+    return orders;
+  }
+
+  /**
+   * 等待订单出现在"我的订单"列表中 (Story 13.3)
+   *
+   * 使用轮询机制等待订单出现,用于验证数据同步
+   *
+   * @param orderName 订单名称
+   * @param timeout 超时时间(ms),默认 10000ms
+   * @returns 是否在超时时间内检测到订单
+   * @example
+   * const found = await talentMiniPage.waitForOrderToAppear('测试订单', 10000);
+   * if (found) {
+   *   console.debug('订单已同步到小程序');
+   * }
+   */
+  async waitForOrderToAppear(orderName: string, timeout: number = 10000): Promise<boolean> {
+    const startTime = Date.now();
+    const pollInterval = 500;
+
+    while (Date.now() - startTime < timeout) {
+      // 刷新订单列表
+      await this.page.evaluate(() => {
+        window.location.reload();
+      });
+      await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.PAGE_LOAD });
+      await this.page.waitForTimeout(TIMEOUTS.SHORT);
+
+      // 检查订单是否出现
+      const orders = await this.getMyOrders();
+      const found = orders.some(order => order.name === orderName);
+
+      if (found) {
+        const syncTime = Date.now() - startTime;
+        console.debug(`[人才小程序] 订单 "${orderName}" 已出现,耗时: ${syncTime}ms`);
+        return true;
+      }
+
+      await this.page.waitForTimeout(pollInterval);
+    }
+
+    console.debug(`[人才小程序] 订单 "${orderName}" 未在 ${timeout}ms 内出现`);
+    return false;
+  }
+
+  /**
+   * 打开订单详情 (Story 13.3)
+   *
+   * @param orderName 订单名称
+   * @returns 订单详情页 URL 中的 ID 参数
+   * @example
+   * const orderId = await talentMiniPage.openOrderDetail('测试订单');
+   * console.debug(`打开了订单详情: ${orderId}`);
+   */
+  async openOrderDetail(orderName: string): Promise<string> {
+    // 查找包含订单名称的卡片并点击
+    const orderCard = this.page.locator('.bg-white.p-4, .card, [class*="order-card"]').filter({ hasText: orderName }).first();
+    await orderCard.click();
+
+    // 等待导航到详情页
+    await this.page.waitForURL(
+      url => url.hash.includes('/pages/talent/order/detail/index') || url.hash.includes('/order/detail'),
+      { timeout: TIMEOUTS.PAGE_LOAD }
+    );
+
+    // 提取详情页 URL 中的 ID 参数
+    const afterUrl = this.page.url();
+    const urlMatch = afterUrl.match(/id[=]?(\d+)/);
+    const orderId = urlMatch ? urlMatch[1] : '';
+
+    console.debug(`[人才小程序] 已打开订单详情: ${orderId}`);
+    return orderId;
+  }
+
+  /**
+   * 获取订单详情信息 (Story 13.3)
+   *
+   * @returns 订单详情数据
+   * @example
+   * const detail = await talentMiniPage.getOrderDetail();
+   * console.debug(`订单详情: ${detail.name}, 状态: ${detail.status}`);
+   */
+  async getOrderDetail(): Promise<TalentOrderDetailData> {
+    const pageContent = await this.page.textContent('body') || '';
+
+    const detail: TalentOrderDetailData = {
+      id: 0,
+      name: '',
+      companyName: '',
+      status: '',
+    };
+
+    // 从 URL 中提取订单 ID
+    const urlMatch = this.page.url().match(/id[=]?(\d+)/);
+    if (urlMatch) {
+      detail.id = parseInt(urlMatch[1], 10);
+    }
+
+    // 提取订单名称
+    const nameMatch = pageContent.match(/订单名称[::]?\s*([^\n]+)/);
+    if (nameMatch) {
+      detail.name = nameMatch[1].trim();
+    } else {
+      // 尝试查找大号标题文本
+      const titleElement = this.page.locator('.text-xl, .text-lg, .font-bold').first();
+      const titleText = await titleElement.textContent();
+      if (titleText) {
+        detail.name = titleText.trim();
+      }
+    }
+
+    // 提取公司名称
+    const companyMatch = pageContent.match(/公司[::]?\s*([^\n]+)/);
+    if (companyMatch) {
+      detail.companyName = companyMatch[1].trim();
+    }
+
+    // 提取平台名称
+    const platformMatch = pageContent.match(/平台[::]?\s*([^\n]+)/);
+    if (platformMatch) {
+      detail.platformName = platformMatch[1].trim();
+    }
+
+    // 提取订单状态
+    const statusKeywords = ['进行中', '已完成', '草稿', '已确认', '未入职', '已入职', '工作中', '已离职'];
+    for (const keyword of statusKeywords) {
+      if (pageContent.includes(keyword)) {
+        detail.status = keyword;
+        break;
+      }
+    }
+
+    // 提取预计人数
+    const expectedCountMatch = pageContent.match(/预计人数[::]?\s*(\d+)/);
+    if (expectedCountMatch) {
+      detail.expectedCount = parseInt(expectedCountMatch[1], 10);
+    }
+
+    // 提取实际人数
+    const actualCountMatch = pageContent.match(/实际人数[::]?\s*(\d+)/);
+    if (actualCountMatch) {
+      detail.actualCount = parseInt(actualCountMatch[1], 10);
+    }
+
+    // 提取预计开始日期
+    const startDateMatch = pageContent.match(/开始日期[::]?\s*(\d{4}-\d{2}-\d{2})/);
+    if (startDateMatch) {
+      detail.expectedStartDate = startDateMatch[1];
+    }
+
+    // 提取薪资
+    const salaryMatch = pageContent.match(/薪资[::]?\s*[¥¥]?(\d+)/);
+    if (salaryMatch) {
+      detail.salary = parseInt(salaryMatch[1], 10);
+    }
+
+    return detail;
+  }
 }

+ 62 - 67
web/tests/e2e/specs/cross-platform/person-add-sync.spec.ts

@@ -2,6 +2,7 @@ import { TIMEOUTS } from '../../utils/timeouts';
 import { test, expect } from '../../utils/test-setup';
 import { AdminLoginPage } from '../../pages/admin/login.page';
 import { TalentMiniPage } from '../../pages/mini/talent-mini.page';
+import type { TalentOrderData, TalentOrderDetailData } from '../../pages/mini/talent-mini.page';
 
 /**
  * 跨端数据同步 E2E 测试 - 人员添加
@@ -114,10 +115,13 @@ test.describe('跨端数据同步测试 - 后台添加人员到人才小程序',
       console.debug('[后台] 打开选择残疾人对话框');
 
       // 5. 选择残疾人
-      // 使用测试残疾人_1768346764677_11_9311(ID: 1239)
       // 注意:已绑定人员的复选框会被禁用
-      const testPersonId = 1239; // 测试残疾人_1768346764677_11_9311 的 ID
-      await adminPage.getByTestId(`person-checkbox-${testPersonId}`).click();
+      // 动态选择第一个未绑定的残疾人
+      const checkboxes = adminPage.locator('[data-testid^="person-checkbox-"]:not([disabled])');
+      const firstAvailable = checkboxes.first();
+      const testPersonIdAttr = await firstAvailable.getAttribute('data-testid');
+      const testPersonId = parseInt(testPersonIdAttr?.replace('person-checkbox-', '') || '0');
+      await firstAvailable.click();
       console.debug(`[后台] 选择残疾人 ID: ${testPersonId}`);
 
       // 6. 点击"确认选择"按钮
@@ -137,7 +141,8 @@ test.describe('跨端数据同步测试 - 后台添加人员到人才小程序',
       await adminPage.waitForTimeout(TIMEOUTS.LONG);
 
       // 9. 验证 Toast 通知
-      const successToast = adminPage.locator('[data-sonner-toast][data-type="success"]');
+      // 使用 filter 精确匹配"批量添加人员成功"消息(避免匹配"已添加到待添加列表")
+      const successToast = adminPage.locator('[data-sonner-toast][data-type="success"]').filter({ hasText: '批量添加人员成功' });
       await expect(successToast).toBeVisible({ timeout: TIMEOUTS.VERY_LONG });
       const toastMessage = await successToast.textContent();
       console.debug(`[后台] Toast 消息: ${toastMessage}`);
@@ -169,25 +174,12 @@ test.describe('跨端数据同步测试 - 后台添加人员到人才小程序',
       const syncStartTime = Date.now();
 
       // 3. 导航到"我的订单"页面
-      // TODO: 实现 TalentMiniPage 的 navigateToMyOrders 方法
-      // await talentMiniPage.navigateToMyOrders();
-      console.debug('[人才小程序] 导航到我的订单');
+      await talentMiniPage.navigateToMyOrders();
+      console.debug('[人才小程序] 已导航到我的订单页面');
 
       // 4. 验证订单显示(使用轮询等待)
       // 由于数据同步可能有延迟,使用轮询检查
-      let orderFound = false;
-      const maxWaitTime = TEST_SYNC_TIMEOUT;
-      const pollInterval = TEST_POLL_INTERVAL;
-
-      const pollStartTime = Date.now();
-      while (Date.now() - pollStartTime < maxWaitTime) {
-        // TODO: 实现 TalentMiniPage 的 getMyOrders 方法
-        // const orders = await talentMiniPage.getMyOrders();
-        // orderFound = orders.some(order => order.name === TEST_ORDER_NAME);
-        // if (orderFound) break;
-
-        await talentMiniPage.waitForTimeout(pollInterval);
-      }
+      const orderFound = await talentMiniPage.waitForOrderToAppear(TEST_ORDER_NAME, TEST_SYNC_TIMEOUT);
 
       const syncEndTime = Date.now();
       testState.syncTime = syncEndTime - syncStartTime;
@@ -202,23 +194,43 @@ test.describe('跨端数据同步测试 - 后台添加人员到人才小程序',
     });
 
     test('应该在小程序订单详情中显示完整信息', async ({ page: talentMiniPage }) => {
-      // 前置条件:已登录
+      // 前置条件:已登录并在我的订单页面
       await loginTalentMini(talentMiniPage);
+      await talentMiniPage.navigateToMyOrders();
 
       // 1. 导航到"我的订单"并找到测试订单
-      // TODO: 实现订单查找逻辑
+      const orders: TalentOrderData[] = await (talentMiniPage as unknown as TalentMiniPage).getMyOrders();
+      const testOrder = orders.find(order => order.name === TEST_ORDER_NAME);
+
+      expect(testOrder, `应该找到订单 "${TEST_ORDER_NAME}"`).toBeDefined();
+      console.debug(`[人才小程序] 找到订单: ${testOrder?.name}`);
 
       // 2. 点击订单详情
-      // TODO: 实现点击订单详情逻辑
+      const orderId = await talentMiniPage.openOrderDetail(TEST_ORDER_NAME);
+      expect(orderId).toBeTruthy();
+      console.debug(`[人才小程序] 已打开订单详情: ${orderId}`);
 
       // 3. 验证订单信息完整性
-      // 验证订单名称、公司、状态等字段
-      console.debug('[人才小程序] 验证订单详情信息');
+      const detail: TalentOrderDetailData = await (talentMiniPage as unknown as TalentMiniPage).getOrderDetail();
+
+      // 验证订单名称
+      expect(detail.name).toBe(TEST_ORDER_NAME);
+      console.debug(`[人才小程序] 订单名称: ${detail.name}`);
+
+      // 验证公司名称
+      expect(detail.companyName).toBeDefined();
+      console.debug(`[人才小程序] 公司名称: ${detail.companyName}`);
+
+      // 验证订单状态
+      expect(detail.status).toBeDefined();
+      console.debug(`[人才小程序] 订单状态: ${detail.status}`);
+
+      console.debug('[人才小程序] 订单详情信息验证完成');
     });
   });
 
   test.describe.serial('数据同步时效性验证', () => {
-    test('应该在合理时间内完成数据同步(≤ 10 秒)', async ({ page: adminPage, page: talentMiniPage, testUsers }) => {
+    test('应该在合理时间内完成数据同步(≤ 10 秒)', async ({ page: _adminPage, page: _talentMiniPage, testUsers: _testUsers }) => {
       // 此测试专门验证数据同步时效性
       // AC6: 数据应在 5 秒内同步,最多等待 10 秒
 
@@ -256,47 +268,30 @@ test.describe('跨端数据同步测试 - 后台添加人员到人才小程序',
       await adminPage.waitForSelector('text=选择残疾人', { state: 'visible', timeout: TIMEOUTS.DIALOG });
 
       // 4. 选择多个残疾人
-      // TODO: 实现多选逻辑
-      console.debug('[后台] 多人员添加测试 - TODO: 实现多选逻辑');
+      // 选择第一个和第二个可用的残疾人(未被绑定的)
+      const checkboxes = adminPage.locator('[data-testid^="person-checkbox-"]:not([disabled])');
+      const checkboxCount = await checkboxes.count();
+
+      if (checkboxCount >= 2) {
+        await checkboxes.nth(0).click();
+        await checkboxes.nth(1).click();
+        console.debug(`[后台] 已选择 ${Math.min(2, checkboxCount)} 个残疾人`);
+
+        // 点击"确认选择"
+        await adminPage.getByTestId('confirm-batch-button').click();
+        await adminPage.waitForTimeout(TIMEOUTS.MEDIUM);
+
+        // 点击"确认添加"
+        await adminPage.getByTestId('confirm-add-persons-button').click();
+        await adminPage.waitForTimeout(TIMEOUTS.LONG);
+
+        // 验证成功 Toast
+        const successToast = adminPage.locator('[data-sonner-toast][data-type="success"]').filter({ hasText: '批量添加人员成功' });
+        await expect(successToast).toBeVisible({ timeout: TIMEOUTS.VERY_LONG });
+        console.debug('[后台] 多人员添加成功');
+      } else {
+        console.debug(`[后台] 可用残疾人不足 2 个,跳过多人员测试`);
+      }
     });
   });
 });
-
-/**
- * 待实现的方法(TalentMiniPage 扩展):
- *
- * // 导航到"我的订单"页面
- * async navigateToMyOrders(): Promise<void>
- *
- * // 获取订单列表
- * async getMyOrders(): Promise<OrderData[]>
- *
- * // 等待订单出现(带超时)
- * async waitForOrderToAppear(orderName: string, timeout?: number): Promise<boolean>
- *
- * // 打开订单详情
- * async openOrderDetail(orderName: string): Promise<void>
- *
- * // 获取订单详情信息
- * async getOrderDetail(): Promise<OrderDetailData>
- *
- * 待实现的数据类型:
- * interface OrderData {
- *   id: number;
- *   name: string;
- *   companyName: string;
- *   status: string;
- *   // ...
- * }
- *
- * interface OrderDetailData {
- *   id: number;
- *   name: string;
- *   platform: string;
- *   company: string;
- *   status: string;
- *   expectedPersons: number;
- *   actualPersons: number;
- *   // ...
- * }
- */