Ver Fonte

test(e2e): 完成 Story 11.4 - Company 管理 Page Object 代码审查

- 集成 @d8d/e2e-test-utils 的 selectRadixOptionAsync 实现 PlatformSelector 选择
- 优化删除方法中的 token 查找策略(仅检查标准键名)
- 清理过多的 console.debug 语句(保留关键错误日志)
- 提取硬编码的 API URL 为类常量
- 更新 fillCompanyForm、createCompany、editCompany 方法签名,添加 platformName 参数
- 更新 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 há 5 dias atrás
pai
commit
b558b9ff9b

+ 10 - 1
_bmad-output/implementation-artifacts/11-4-company-page-object.story.md

@@ -1,6 +1,6 @@
 # Story 11.4: Company 管理 Page Object(重点)
 
-Status: review
+Status: done
 
 <!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
 
@@ -622,6 +622,13 @@ Claude (d8d-model)
 8. ✅ 添加 companyManagementPage fixture 到 test-setup.ts
 9. ✅ TypeScript 类型检查通过(无类型错误)
 
+**代码审查完成 (2026-01-12):**
+1. ✅ 集成 `@d8d/e2e-test-utils` 的 `selectRadixOptionAsync` 实现 PlatformSelector 选择
+2. ✅ 优化删除方法中的 token 查找策略(仅检查标准键名)
+3. ✅ 清理过多的 console.debug 语句(保留关键错误日志)
+4. ✅ 提取硬编码的 API URL 为类常量
+5. ✅ 更新 `fillCompanyForm`、`createCompany`、`editCompany` 方法签名,添加 `platformName` 参数
+
 **Story 文件位置:**
 - `_bmad-output/implementation-artifacts/11-4-company-page-object.story.md`
 
@@ -632,6 +639,8 @@ Claude (d8d-model)
 - API 直接删除策略(绕过 UI 不稳定性)
 - 完整的 JSDoc 注释
 - 导出 CompanyStatus 常量和类型
+- 集成 @d8d/e2e-test-utils 实现 PlatformSelector 选择
+- API URL 配置化为类常量
 
 ### File List
 

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

@@ -173,7 +173,7 @@ development_status:
   11-1-platform-page-object: done         # Platform 管理 Page Object ✅ 完成(代码审查完成,所有HIGH和MEDIUM问题已修复)
   11-2-platform-create-test: done   # 创建测试平台 ✅ 10 passed (33.6s) - 已修复删除超时和网络监听问题 (2026-01-12)
   11-3-platform-list-test: done         # 验证平台列表显示 - 代码审查完成,所有 HIGH 和 MEDIUM 问题已修复
-  11-4-company-page-object: review  # Company 管理 Page Object(重点)
+  11-4-company-page-object: done  # Company 管理 Page Object(重点) - 代码审查完成,所有 HIGH 和 MEDIUM 问题已修复
   11-5-company-create-test: backlog        # 创建测试公司(需要先有平台)
   11-6-company-list-test: backlog          # 验证公司列表显示
   11-7-channel-page-object: backlog        # Channel 管理 Page Object(可选)

+ 51 - 60
web/tests/e2e/pages/admin/company-management.page.ts

@@ -1,4 +1,10 @@
 import { Page, Locator } from '@playwright/test';
+import { selectRadixOptionAsync } from '@d8d/e2e-test-utils';
+
+/**
+ * API 基础 URL
+ */
+const API_BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:8080';
 
 /**
  * 公司状态常量
@@ -91,6 +97,12 @@ export interface FormSubmitResult {
 export class CompanyManagementPage {
   readonly page: Page;
 
+  // ===== API 端点常量 =====
+  /** 获取所有公司列表 API */
+  private static readonly API_GET_ALL_COMPANIES = `${API_BASE_URL}/api/v1/company/getAllCompanies`;
+  /** 删除公司 API */
+  private static readonly API_DELETE_COMPANY = `${API_BASE_URL}/api/v1/company/deleteCompany`;
+
   // ===== 页面级选择器 =====
   /** 页面标题 */
   readonly pageTitle: Locator;
@@ -237,18 +249,16 @@ export class CompanyManagementPage {
   /**
    * 填写公司表单
    * @param data 公司数据
+   * @param platformName 平台名称(当需要选择平台时必须提供)
    */
-  async fillCompanyForm(data: CompanyData): Promise<void> {
+  async fillCompanyForm(data: CompanyData, platformName?: string): Promise<void> {
     // 等待表单出现
     await this.page.waitForSelector('form', { state: 'visible', timeout: 5000 });
 
     // 填写平台选择器(可选字段)
-    if (data.platformId !== undefined) {
-      // 注意:PlatformSelector 使用 Radix UI Select
-      // 需要使用 @d8d/e2e-test-utils 的 selectRadixOption 工具
-      // 这里假设测试代码中会传入平台名称进行选择
-      // 实际的选择逻辑应在测试代码中处理
-      console.debug('需要选择平台,platformId:', data.platformId);
+    if (data.platformId !== undefined && platformName) {
+      // 使用 @d8d/e2e-test-utils 的 selectRadixOptionAsync 选择平台
+      await selectRadixOptionAsync(this.page, '平台', platformName);
     }
 
     // 填写公司名称(必填字段)
@@ -314,7 +324,6 @@ export class CompanyManagementPage {
         submitButton = this.page.getByRole('button', { name: /^(创建|更新|保存)$/ });
       }
 
-      console.debug('点击提交按钮,按钮数量:', await submitButton.count());
       await submitButton.click();
 
       // 等待 API 响应并收集
@@ -340,11 +349,6 @@ export class CompanyManagementPage {
           responseHeaders: await mainResponse.allHeaders().catch(() => ({})),
           responseBody: jsonBody || responseBody,
         });
-        console.debug('公司 API 响应:', {
-          url: mainResponse.url(),
-          status: mainResponse.status(),
-          ok: mainResponse.ok()
-        });
       }
 
       if (getAllResponse) {
@@ -361,18 +365,13 @@ export class CompanyManagementPage {
           responseHeaders: await getAllResponse.allHeaders().catch(() => ({})),
           responseBody: jsonBody || responseBody,
         });
-        console.debug('getAllCompanies API 响应:', {
-          url: getAllResponse.url(),
-          status: getAllResponse.status(),
-          ok: getAllResponse.ok()
-        });
       }
 
       // 等待网络请求完成
       try {
         await this.page.waitForLoadState('networkidle', { timeout: 5000 });
       } catch {
-        console.debug('networkidle 超时,继续检查 Toast 消息');
+        // 继续检查 Toast 消息
       }
     } catch (error) {
       console.debug('submitForm 异常:', error);
@@ -425,15 +424,6 @@ export class CompanyManagementPage {
       successMessage = await ((await successToast.count()) > 0 ? successToast.first() : fallbackSuccessToast).textContent();
     }
 
-    // 调试输出
-    console.debug('submitForm 结果:', {
-      hasError,
-      hasSuccess,
-      errorMessage,
-      successMessage,
-      responsesCount: responses.length
-    });
-
     return {
       success: hasSuccess || (!hasError && !hasSuccess && responses.some(r => r.ok)),
       hasError,
@@ -461,13 +451,14 @@ export class CompanyManagementPage {
     const count = await dialog.count();
 
     if (count === 0) {
-      console.debug('对话框已经不存在,跳过等待');
       return;
     }
 
     // 等待对话框隐藏
     await dialog.waitFor({ state: 'hidden', timeout: 5000 })
-      .catch(() => console.debug('对话框关闭超时,可能已经关闭'));
+      .catch(() => {
+        // 对话框可能已经关闭
+      });
 
     // 额外等待以确保 DOM 更新完成
     await this.page.waitForTimeout(500);
@@ -480,7 +471,9 @@ export class CompanyManagementPage {
     await this.confirmDeleteButton.click();
     // 等待确认对话框关闭和网络请求完成
     await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
-      .catch(() => console.debug('删除确认对话框关闭超时'));
+      .catch(() => {
+        // 继续执行
+      });
     try {
       await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 });
     } catch {
@@ -496,7 +489,9 @@ export class CompanyManagementPage {
     const cancelButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: '取消' });
     await cancelButton.click();
     await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
-      .catch(() => console.debug('删除确认对话框关闭超时(取消操作)'));
+      .catch(() => {
+        // 继续执行
+      });
   }
 
   // ===== CRUD 操作方法 =====
@@ -504,11 +499,12 @@ export class CompanyManagementPage {
   /**
    * 创建公司(完整流程)
    * @param data 公司数据
+   * @param platformName 平台名称(当需要选择平台时必须提供)
    * @returns 表单提交结果
    */
-  async createCompany(data: CompanyData): Promise<FormSubmitResult> {
+  async createCompany(data: CompanyData, platformName?: string): Promise<FormSubmitResult> {
     await this.openCreateDialog();
-    await this.fillCompanyForm(data);
+    await this.fillCompanyForm(data, platformName);
     const result = await this.submitForm();
     await this.waitForDialogClosed();
     return result;
@@ -518,11 +514,12 @@ export class CompanyManagementPage {
    * 编辑公司(完整流程)
    * @param companyName 公司名称
    * @param data 更新的公司数据
+   * @param platformName 平台名称(当需要选择平台时必须提供)
    * @returns 表单提交结果
    */
-  async editCompany(companyName: string, data: CompanyData): Promise<FormSubmitResult> {
+  async editCompany(companyName: string, data: CompanyData, platformName?: string): Promise<FormSubmitResult> {
     await this.openEditDialog(companyName);
-    await this.fillCompanyForm(data);
+    await this.fillCompanyForm(data, platformName);
     const result = await this.submitForm();
     await this.waitForDialogClosed();
     return result;
@@ -537,32 +534,19 @@ export class CompanyManagementPage {
     try {
       // 使用 API 直接删除,添加超时保护
       const result = await Promise.race([
-        this.page.evaluate(async ({ companyName }) => {
-          // 尝试多种可能的 token 键名
-          let token = localStorage.getItem('token');
-          if (!token) {
-            token = localStorage.getItem('auth_token');
-          }
-          if (!token) {
-            token = localStorage.getItem('accessToken');
-          }
-          if (!token) {
-            const localStorageKeys = Object.keys(localStorage);
-            for (const key of localStorageKeys) {
-              if (key.toLowerCase().includes('token')) {
-                token = localStorage.getItem(key);
-                break;
-              }
-            }
-          }
+        this.page.evaluate(async ({ companyName, apiGetAll, apiDelete }) => {
+          // 尝试获取 token(使用标准键名)
+          let token = localStorage.getItem('token') ||
+                      localStorage.getItem('auth_token') ||
+                      localStorage.getItem('accessToken');
 
           if (!token) {
-            return { success: false, notFound: true };
+            return { success: false, noToken: true };
           }
 
           try {
             // 先获取公司列表,找到公司的 ID(限制 100 条)
-            const listResponse = await fetch('http://localhost:8080/api/v1/company/getAllCompanies?skip=0&take=100', {
+            const listResponse = await fetch(`${apiGetAll}?skip=0&take=100`, {
               headers: { 'Authorization': `Bearer ${token}` }
             });
 
@@ -583,7 +567,7 @@ export class CompanyManagementPage {
             }
 
             // 使用公司 ID 删除 - POST 方法
-            const deleteResponse = await fetch('http://localhost:8080/api/v1/company/deleteCompany', {
+            const deleteResponse = await fetch(apiDelete, {
               method: 'POST',
               headers: {
                 'Authorization': `Bearer ${token}`,
@@ -600,19 +584,26 @@ export class CompanyManagementPage {
           } catch (error) {
             return { success: false, notFound: false };
           }
-        }, { companyName }),
+        }, {
+          companyName,
+          apiGetAll: CompanyManagementPage.API_GET_ALL_COMPANIES,
+          apiDelete: CompanyManagementPage.API_DELETE_COMPANY
+        }),
         // 10 秒超时
         new Promise((resolve) => setTimeout(() => resolve({ success: false, timeout: true }), 10000))
       ]) as any;
 
       // 如果超时或公司找不到,返回 true(允许测试继续)
       if (result.timeout || result.notFound) {
-        console.debug(`删除公司 "${companyName}" 超时或未找到,跳过`);
         return true;
       }
 
+      if (result.noToken) {
+        console.debug('删除公司失败: 未找到认证 token');
+        return false;
+      }
+
       if (!result.success) {
-        console.debug(`删除公司 "${companyName}" 失败:`, result.error);
         return false;
       }