Jelajahi Sumber

test(e2e): 完成 Story 10.1 - 订单管理 Page Object 代码审查

修复的问题:
- HIGH-1: 补充 getOrderDetailInfo() 方法完整字段获取
- HIGH-2/4: 修复 setFilters() workStatus 筛选逻辑
- HIGH-3: 网络监听器使用 try-finally 防止内存泄漏
- MED-1: 删除未使用的 selectRadixOptionAsync 导入
- MED-4: 改进 openPersonManagementDialog() JSDoc 注释
- MED-5: 对话框关闭错误处理添加调试日志
- CRIT-1: 更新 Story 状态为 done

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 1 Minggu lalu
induk
melakukan
a059239cc8

+ 1 - 1
_bmad-output/implementation-artifacts/10-1-order-page-object.md

@@ -1,6 +1,6 @@
 # Story 10.1: 创建订单管理 Page Object
 
-Status: review
+Status: done
 
 <!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
 

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

@@ -113,7 +113,7 @@ development_status:
   epic-8: in-progress
   8-1-region-page-object: done         # 创建区域管理 Page Object
   8-2-region-list-test: done          # 编写区域列表查看测试(代码审查已完成)
-  8-3-add-region-test: ready-for-dev    # 编写添加区域测试
+  8-3-add-region-test: in-progress      # 编写添加区域测试
   8-4-edit-region-test: backlog          # 编写编辑区域测试
   8-5-delete-region-test: backlog        # 编写删除区域测试
   8-6-cascade-select-test: backlog       # 编写级联选择完整流程测试
@@ -130,7 +130,7 @@ development_status:
   # 详情参见: _bmad-output/implementation-artifacts/epic-9-plan.md
   epic-9: in-progress
   9-1-photo-upload-tests: done              # 照片上传功能完整测试(所有8个测试通过)
-  9-2-bankcard-tests: ready-for-dev      # 银行卡管理功能测试(添加、编辑、删除)
+  9-2-bankcard-tests: review      # 银行卡管理功能测试(添加、编辑、删除)
   9-3-note-tests: backlog                # 备注管理功能测试(添加、修改、删除)
   9-4-visit-tests: backlog               # 回访记录管理测试(创建、查看、编辑)
   9-5-crud-tests: backlog                # 完整流程测试(新增、编辑、删除、查看)
@@ -146,7 +146,7 @@ development_status:
   # 依赖: Epic 1 和 Epic 2 完成(Select 工具基础)
   # 详情参见: _bmad-output/planning-artifacts/epics.md (Epic 10)
   epic-10: in-progress
-  10-1-order-page-object: review                # 创建订单管理 Page Object
+  10-1-order-page-object: done                  # 创建订单管理 Page Object
   10-2-order-list-tests: backlog           # 编写订单列表查看测试
   10-3-order-filter-tests: backlog         # 编写订单搜索和筛选测试
   10-4-order-create-tests: backlog         # 编写创建订单测试

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

@@ -1,5 +1,5 @@
 import { Page, Locator } from '@playwright/test';
-import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
+import { selectRadixOption } from '@d8d/e2e-test-utils';
 
 /**
  * 订单状态常量
@@ -233,15 +233,19 @@ export class OrderManagementPage {
     dateRange?: { start?: string; end?: string };
   }) {
     // 订单状态筛选
-    if (filters.status || filters.workStatus) {
-      const statusFilter = this.page.getByLabel(/订单状态|状态/);
+    if (filters.status) {
+      const statusFilter = this.page.getByLabel(/订单状态/);
       await statusFilter.click();
-      const statusLabel = filters.status
-        ? ORDER_STATUS_LABELS[filters.status]
-        : undefined;
-      if (statusLabel) {
-        await this.page.getByRole('option', { name: statusLabel }).click();
-      }
+      const statusLabel = ORDER_STATUS_LABELS[filters.status];
+      await this.page.getByRole('option', { name: statusLabel }).click();
+    }
+
+    // 工作状态筛选
+    if (filters.workStatus) {
+      const workStatusFilter = this.page.getByLabel(/工作状态/);
+      await workStatusFilter.click();
+      const workStatusLabel = WORK_STATUS_LABELS[filters.workStatus];
+      await this.page.getByRole('option', { name: workStatusLabel }).click();
     }
 
     // 平台筛选
@@ -407,15 +411,17 @@ export class OrderManagementPage {
 
     this.page.on('response', responseHandler);
 
-    // 点击提交按钮(创建或更新)
-    const submitButton = this.page.getByRole('button', { name: /^(创建|更新|保存)$/ });
-    await submitButton.click();
-
-    // 等待网络请求完成
-    await this.page.waitForLoadState('networkidle', { timeout: 10000 });
+    try {
+      // 点击提交按钮(创建或更新)
+      const submitButton = this.page.getByRole('button', { name: /^(创建|更新|保存)$/ });
+      await submitButton.click();
 
-    // 移除监听器
-    this.page.off('response', responseHandler);
+      // 等待网络请求完成
+      await this.page.waitForLoadState('networkidle', { timeout: 10000 });
+    } finally {
+      // 确保监听器总是被移除,防止内存泄漏
+      this.page.off('response', responseHandler);
+    }
 
     // 等待 Toast 消息显示
     await this.page.waitForTimeout(2000);
@@ -461,7 +467,8 @@ export class OrderManagementPage {
    */
   async waitForDialogClosed() {
     const dialog = this.page.locator('[role="dialog"]');
-    await dialog.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
+    await dialog.waitFor({ state: 'hidden', timeout: 5000 })
+      .catch(() => console.debug('对话框关闭超时,可能已经关闭'));
     await this.page.waitForTimeout(500);
   }
 
@@ -472,7 +479,8 @@ export class OrderManagementPage {
     const confirmButton = this.page.getByRole('button', { name: /^确认删除$/ });
     await confirmButton.click();
     // 等待确认对话框关闭和网络请求完成
-    await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
+    await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
+      .catch(() => console.debug('删除确认对话框关闭超时'));
     await this.page.waitForLoadState('networkidle', { timeout: 10000 });
     await this.page.waitForTimeout(1000);
   }
@@ -485,7 +493,8 @@ export class OrderManagementPage {
       this.page.locator('[role="alertdialog"]')
     );
     await cancelButton.click();
-    await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
+    await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
+      .catch(() => console.debug('删除确认对话框关闭超时(取消操作)'));
   }
 
   /**
@@ -525,24 +534,63 @@ export class OrderManagementPage {
     company?: string;
     channel?: string;
   }> {
-    const result: Record<string, string> = {};
+    const dialog = this.page.locator('[role="dialog"]');
+    const result: Record<string, string | undefined> = {};
 
-    // 订单名称
-    const nameElement = this.page.locator('[role="dialog"]').getByText(/订单名称/);
+    // 订单名称 - 查找"订单名称"标签后的值
+    const nameElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单名称' })
+      .locator('..').locator('p,span,div').nth(1);
     if (await nameElement.count() > 0) {
-      result.name = await nameElement.textContent();
+      const text = await nameElement.textContent();
+      result.name = text || undefined;
     }
 
     // 订单状态
-    const statusElement = this.page.locator('[role="dialog"]').getByText(/订单状态/);
+    const statusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单状态' })
+      .locator('..').locator('p,span,div').nth(1);
     if (await statusElement.count() > 0) {
-      result.status = await statusElement.textContent();
+      const text = await statusElement.textContent();
+      result.status = text || undefined;
     }
 
     // 工作状态
-    const workStatusElement = this.page.locator('[role="dialog"]').getByText(/工作状态/);
+    const workStatusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '工作状态' })
+      .locator('..').locator('p,span,div').nth(1);
     if (await workStatusElement.count() > 0) {
-      result.workStatus = await workStatusElement.textContent();
+      const text = await workStatusElement.textContent();
+      result.workStatus = text || undefined;
+    }
+
+    // 预计开始日期
+    const startDateElement = dialog.locator('.text-muted-foreground').filter({ hasText: /预计开始日期|开始日期/ })
+      .locator('..').locator('p,span,div').nth(1);
+    if (await startDateElement.count() > 0) {
+      const text = await startDateElement.textContent();
+      result.expectedStartDate = text || undefined;
+    }
+
+    // 平台
+    const platformElement = dialog.locator('.text-muted-foreground').filter({ hasText: '平台' })
+      .locator('..').locator('p,span,div').nth(1);
+    if (await platformElement.count() > 0) {
+      const text = await platformElement.textContent();
+      result.platform = text || undefined;
+    }
+
+    // 公司
+    const companyElement = dialog.locator('.text-muted-foreground').filter({ hasText: '公司' })
+      .locator('..').locator('p,span,div').nth(1);
+    if (await companyElement.count() > 0) {
+      const text = await companyElement.textContent();
+      result.company = text || undefined;
+    }
+
+    // 渠道
+    const channelElement = dialog.locator('.text-muted-foreground').filter({ hasText: '渠道' })
+      .locator('..').locator('p,span,div').nth(1);
+    if (await channelElement.count() > 0) {
+      const text = await channelElement.textContent();
+      result.channel = text || undefined;
     }
 
     return result;
@@ -552,7 +600,22 @@ export class OrderManagementPage {
 
   /**
    * 打开人员管理对话框
-   * @param orderName 订单名称(如果在订单列表页)
+   *
+   * **使用场景:**
+   * - **从订单列表页打开**: 传入 `orderName` 参数,方法会先找到对应订单行,再点击人员管理按钮
+   * - **从订单详情页打开**: 不传参数,方法会直接点击页面中的人员管理按钮
+   *
+   * @param orderName 订单名称(可选)。从列表页打开时需要传入,从详情页打开时不传
+   *
+   * @example
+   * ```typescript
+   * // 从订单列表页打开
+   * await orderPage.openPersonManagementDialog('测试订单');
+   *
+   * // 从订单详情页打开
+   * await orderPage.openDetailDialog('测试订单');
+   * await orderPage.openPersonManagementDialog();
+   * ```
    */
   async openPersonManagementDialog(orderName?: string) {
     // 如果提供了订单名称,先找到对应的订单行