11-4-company-page-object.story.md 23 KB

Story 11.4: Company 管理 Page Object(重点)

Status: done

Story

作为测试开发者, 我想要创建公司管理的 Page Object, 以便组织公司管理相关的页面元素和操作,为后续编写公司管理 E2E 测试提供基础。

Acceptance Criteria

  1. AC1: 创建 CompanyManagementPage 文件

    • 文件路径: web/tests/e2e/pages/admin/company-management.page.ts
    • 使用 TypeScript 编写
    • 导入 Playwright 的 Page、Locator、Response 等类型
    • 定义完整的类结构和方法签名
  2. AC2: 定义页面选择器

    • 定义页面级选择器:pageTitle, createCompanyButton, searchInput, searchButton, companyTable
    • 定义对话框选择器:createDialogTitle, editDialogTitle
    • 定义表单字段选择器:platformSelector, companyNameInput, contactPersonInput, contactPhoneInput, contactEmailInput, addressInput
    • 定义按钮选择器:createSubmitButton, updateSubmitButton, cancelButton, confirmDeleteButton
    • 所有选择器使用 data-testid 定位(遵循测试标准)
  3. AC3: 实现导航和基础验证方法

    • 实现 goto() 方法:导航到 /admin/companies 页面
    • 实现 expectToBeVisible() 方法:验证页面关键元素可见
    • 等待页面标题和表格数据加载完成
  4. AC4: 实现对话框操作方法

    • 实现 openCreateDialog() 方法:打开创建公司对话框
    • 实现 openEditDialog(companyName) 方法:打开编辑公司对话框
    • 实现 openDeleteDialog(companyName) 方法:打开删除确认对话框
    • 实现 cancelDialog() 方法:取消对话框
    • 实现 waitForDialogClosed() 方法:等待对话框关闭
  5. AC5: 实现表单操作方法

    • 实现 fillCompanyForm(data) 方法:填写公司表单
    • 实现 submitForm() 方法:提交表单并捕获网络响应
    • 表单数据接口包含:platformId?, companyName, contactPerson?, contactPhone?, contactEmail?, address?
    • 支持创建和编辑两种表单模式
  6. AC6: 实现 CRUD 操作方法

    • 实现 createCompany(data) 方法:创建公司(完整流程)
    • 实现 editCompany(companyName, data) 方法:编辑公司(完整流程)
    • 实现 deleteCompany(companyName) 方法:删除公司(使用 API 直接删除)
    • 删除成功后刷新页面确保列表更新
  7. AC7: 实现搜索和验证方法

    • 实现 searchByName(name) 方法:按公司名称搜索
    • 实现 companyExists(companyName) 方法:验证公司是否存在(使用精确匹配)
    • 搜索后验证结果
  8. AC8: 定义数据接口和类型

    • 定义 CompanyData 接口(包含所有公司字段)
    • 定义 NetworkResponse 接口(捕获网络响应)
    • 定义 FormSubmitResult 接口(表单提交结果)
    • 所有接口使用 JSDoc 注释
  9. AC9: 代码质量标准

    • TypeScript 类型检查通过(无类型错误)
    • 所有公共方法有完整的 JSDoc 注释
    • 代码结构与 PlatformManagementPage 保持一致

Tasks / Subtasks

  • [x] 任务 1: 创建文件和基础结构 (AC: 1, 8, 9)

    • 创建文件 web/tests/e2e/pages/admin/company-management.page.ts
    • 导入 Playwright 类型和依赖
    • 定义类结构:export class CompanyManagementPage
    • 定义接口:CompanyData, NetworkResponse, FormSubmitResult
  • [x] 任务 2: 定义页面选择器 (AC: 2, 9)

    • 定义页面级选择器(pageTitle, createCompanyButton, searchInput, searchButton, companyTable)
    • 定义对话框选择器(createDialogTitle, editDialogTitle)
    • 定义表单字段选择器(使用 data-testid)
    • 定义按钮选择器(createSubmitButton, updateSubmitButton, cancelButton, confirmDeleteButton)
  • [x] 任务 3: 实现导航和基础验证方法 (AC: 3, 9)

    • 实现 goto() 方法
    • 实现 expectToBeVisible() 方法
    • 添加 JSDoc 注释
  • [x] 任务 4: 实现对话框操作方法 (AC: 4, 9)

    • 实现 openCreateDialog() 方法
    • 实现 openEditDialog(companyName) 方法
    • 实现 openDeleteDialog(companyName) 方法
    • 实现 cancelDialog()waitForDialogClosed() 方法
    • 添加 JSDoc 注释
  • [x] 任务 5: 实现表单操作方法 (AC: 5, 9)

    • 实现 fillCompanyForm(data) 方法
    • 实现 submitForm() 方法(包含网络响应捕获)
    • 处理 Toast 消息验证
    • 添加 JSDoc 注释
  • [x] 任务 6: 实现 CRUD 操作方法 (AC: 6, 9)

    • 实现 createCompany(data) 方法
    • 实现 editCompany(companyName, data) 方法
    • 实现 deleteCompany(companyName) 方法(API 直接删除)
    • 添加 JSDoc 注释
  • [x] 任务 7: 实现搜索和验证方法 (AC: 7, 9)

    • 实现 searchByName(name) 方法
    • 实现 companyExists(companyName) 方法
    • 添加 JSDoc 注释
  • [x] 任务 8: TypeScript 类型检查和代码质量验证 (AC: 9)

    • 运行 pnpm typecheck 检查类型
    • 确保无类型错误
    • 验证代码风格符合项目标准

Dev Notes

Epic 11 背景和目标

Epic 11: 基础配置管理测试 (Epic F)

为平台、公司、渠道配置管理编写 E2E 测试,为后续用户管理和跨端测试提供必要的测试数据。

实体关系链:

Platform (平台)
  ↓ 1:N
Company (公司) - 必须 platformId
  ↓ 1:N
Order (订单) - 必须 companyId

Story 11.4 在 Epic 中的位置:

  • Story 11.1 (已完成) → Story 11.2 (已完成) → Story 11.3 (已完成) → Story 11.4 (当前)
  • Story 11.1 创建了 PlatformManagementPage Page Object
  • Story 11.2 编写了平台创建功能的 E2E 测试
  • Story 11.3 编写了平台列表显示验证测试
  • Story 11.4 将创建 CompanyManagementPage Page Object

Company 实体关键特点:

  • 必须关联 Platform(platformId 外键,可选但建议填写)
  • 公司名称在同一平台下必须唯一(复合唯一索引)
  • 包含联系人信息(可选字段)
  • 有状态字段(status: 1-启用,0-禁用)
  • 删除已禁用的公司才能删除

架构模式和约束

参考 Story 11.1 的 PlatformManagementPage 模式:

  1. 选择器策略:

    • 优先使用 data-testid 定位元素
    • 使用 role + name 组合作为备用(如按钮)
    • 避免使用不稳定的 CSS 类选择器
  2. 网络响应捕获:

    • 使用 waitForResponse() 捕获特定 API 响应
    • 监听 createCompanyupdateCompanygetAllCompanies 等 API
    • 使用 Promise.race() 添加超时保护
  3. Toast 消息验证:

    • Toast 使用 Sonner UI 库:[data-sonner-toast][data-type="error"][data-type="success"]
    • Toast 可能出现得很快,需要主动等待
    • 同时验证 API 响应作为主要验证方式
  4. 删除操作策略:

    • 使用 API 直接删除,绕过 UI 的不可靠性
    • 使用 page.evaluate() 在浏览器上下文中执行 fetch
    • 删除成功后刷新页面确保列表更新

Company 实体结构

Company Entity (来自 company.entity.ts):

{
  id: number;              // 主键ID
  platformId: number | null;  // 平台ID(外键)
  companyName: string;     // 公司名称(必填,最大100字符)
  contactPerson: string | null;   // 联系人(可选,最大50字符)
  contactPhone: string | null;   // 联系电话(可选,最大20字符)
  contactEmail: string | null;   // 联系邮箱(可选,最大100字符)
  address: string | null;        // 地址(可选,最大200字符)
  status: number;          // 状态:1-正常,0-禁用(默认1)
  createTime: Date;        // 创建时间
  updateTime: Date;        // 更新时间
  platform?: Platform;     // 关联的平台信息(eager加载)
}

唯一约束:

  • 同一平台下公司名称唯一:idx_company_name_platform (companyName, platformId)

CompanyManagement UI 组件分析

页面路径: /admin/companies

UI 组件结构(来自 CompanyManagement.tsx):

  1. 页面结构:

    • 页面标题:"公司管理"
    • 搜索栏:搜索输入框 + 搜索按钮 + 创建公司按钮
    • 公司列表表格(7列):公司名称、平台、联系人、联系电话、状态、创建时间、操作
    • 分页组件(DataTablePagination)
  2. 创建/编辑对话框:

    • 对话框标题:"创建公司" / "编辑公司"
    • 表单字段:
      • platformId: PlatformSelector(使用 Radix UI Select,可选)
      • companyName: Input(必填)
      • contactPerson: Input(可选)
      • contactPhone: Input(可选)
      • contactEmail: Input(可选,email 类型)
      • address: Input(可选)
    • 提交按钮:"创建" / "更新"
    • 取消按钮:"取消"
  3. 删除确认对话框:

    • 标题:"确认删除"
    • 描述:"确定要删除这个公司吗?此操作不可恢复。"
    • 确认按钮:"确认删除"(红色 destructive 样式)
    • 取消按钮:"取消"
  4. data-testid 属性(关键):

    搜索相关:
    - search-company-input (搜索输入框)
    - search-company-button (搜索按钮)
    - create-company-button (创建公司按钮)
    
    表单字段(创建):
    - create-company-platform-selector (平台选择器)
    - create-company-name-input (公司名称)
    - create-company-contact-person-input (联系人)
    - create-company-contact-phone-input (联系电话)
    - create-company-contact-email-input (联系邮箱)
    - create-company-address-input (地址)
    - submit-create-company-button (提交按钮)
    - cancel-company-button (取消按钮)
    
    表单字段(编辑):
    - edit-company-platform-selector
    - edit-company-name-input
    - edit-company-contact-person-input
    - edit-company-contact-phone-input
    - edit-company-contact-email-input
    - edit-company-address-input
    - submit-edit-company-button (提交按钮)
    - cancel-edit-company-button (取消按钮)
    
    删除对话框:
    - cancel-delete-company-button (取消)
    - confirm-delete-company-button (确认删除)
    
    表格操作按钮:
    - edit-company-button-{id} (编辑按钮,动态ID)
    - delete-company-button-{id} (删除按钮,动态ID)
    
    其他:
    - company-modal-title (对话框标题)
    
  5. PlatformSelector 集成:

    • 使用 Radix UI Select 组件
    • 使用 @d8d/e2e-test-utilsselectRadixOptionselectRadixOptionAsync
    • 标签:"平台" 或 "选择平台"
    • 选项是异步加载的(从平台列表 API)

API 端点参考

公司管理 API 端点:

// 获取所有公司
GET /api/v1/company/getAllCompanies?skip=0&take=10

// 搜索公司
GET /api/v1/company/searchCompanies?name=xxx&skip=0&take=10

// 创建公司
POST /api/v1/company/createCompany
Body: { platformId?: number, companyName: string, contactPerson?, contactPhone?, contactEmail?, address? }

// 更新公司
POST /api/v1/company/updateCompany
Body: { id: number, platformId?, companyName?, contactPerson?, contactPhone?, contactEmail?, address? }

// 删除公司
POST /api/v1/company/deleteCompany
Body: { id: number }

Page Object 设计参考

CompanyManagementPage 类结构:

import { Page, Locator } from '@playwright/test';

/**
 * 公司数据接口
 */
export interface CompanyData {
  /** 平台ID(可选) */
  platformId?: number;
  /** 公司名称(必填) */
  companyName: string;
  /** 联系人(可选) */
  contactPerson?: string;
  /** 联系电话(可选) */
  contactPhone?: string;
  /** 联系邮箱(可选) */
  contactEmail?: string;
  /** 地址(可选) */
  address?: string;
}

/**
 * 网络响应数据接口
 */
export interface NetworkResponse {
  url: string;
  method: string;
  status: number;
  ok: boolean;
  responseHeaders: Record<string, string>;
  responseBody: unknown;
}

/**
 * 表单提交结果接口
 */
export interface FormSubmitResult {
  success: boolean;
  hasError: boolean;
  hasSuccess: boolean;
  errorMessage?: string;
  successMessage?: string;
  responses?: NetworkResponse[];
}

/**
 * 公司管理 Page Object
 *
 * 用于公司管理功能的 E2E 测试
 * 页面路径: /admin/companies
 */
export class CompanyManagementPage {
  readonly page: Page;

  // ===== 页面级选择器 =====
  readonly pageTitle: Locator;
  readonly createCompanyButton: Locator;
  readonly searchInput: Locator;
  readonly searchButton: Locator;
  readonly companyTable: Locator;

  // ===== 对话框选择器 =====
  readonly createDialogTitle: Locator;
  readonly editDialogTitle: Locator;

  // ===== 表单字段选择器 =====
  readonly platformSelector: Locator;      // PlatformSelector 容器
  readonly companyNameInput: Locator;
  readonly contactPersonInput: Locator;
  readonly contactPhoneInput: Locator;
  readonly contactEmailInput: Locator;
  readonly addressInput: Locator;

  // ===== 按钮选择器 =====
  readonly createSubmitButton: Locator;
  readonly updateSubmitButton: Locator;
  readonly cancelButton: Locator;
  readonly confirmDeleteButton: Locator;

  constructor(page: Page) {
    this.page = page;
    // 初始化所有选择器
  }

  // ===== 导航和基础验证 =====
  async goto(): Promise<void>
  async expectToBeVisible(): Promise<void>

  // ===== 对话框操作 =====
  async openCreateDialog(): Promise<void>
  async openEditDialog(companyName: string): Promise<void>
  async openDeleteDialog(companyName: string): Promise<void>
  async cancelDialog(): Promise<void>
  async waitForDialogClosed(): Promise<void>

  // ===== 表单操作 =====
  async fillCompanyForm(data: CompanyData): Promise<void>
  async submitForm(): Promise<FormSubmitResult>

  // ===== CRUD 操作 =====
  async createCompany(data: CompanyData): Promise<FormSubmitResult>
  async editCompany(companyName: string, data: CompanyData): Promise<FormSubmitResult>
  async deleteCompany(companyName: string): Promise<boolean>

  // ===== 搜索和验证 =====
  async searchByName(name: string): Promise<boolean>
  async companyExists(companyName: string): Promise<boolean>
}

PlatformSelector 选择处理

关键注意事项:

  1. 使用 @d8d/e2e-test-utils 的 Select 工具:

    import { selectRadixOption } from '@d8d/e2e-test-utils';
    
    // 在 fillCompanyForm 中选择平台
    if (data.platformId !== undefined) {
     // 需要知道平台的名称来选择
     // 选项格式:平台名称作为显示值,platformId 作为实际值
     await selectRadixOption(this.page, '平台', platformName);
    }
    
  2. 测试数据准备:

    • 测试需要先创建测试用的平台
    • 可以在测试用例中先创建平台,然后获取平台 ID 和名称
    • 或者使用现有平台数据

与 PlatformManagementPage 的差异

相同部分:

  • 整体结构和模式完全一致
  • 选择器命名规则一致
  • 对话框操作逻辑一致
  • 表单提交流程一致
  • Toast 验证方式一致
  • API 删除策略一致

差异部分:

  1. 额外的表单字段:

    • Company 有 address 字段(Platform 没有)
    • Platform 有 platformName,Company 有 companyName
  2. 外键关系:

    • Company 必须选择关联的 Platform(使用 PlatformSelector)
    • Platform 是顶级实体,无外键
  3. 表格列差异:

    • Platform 表格:7列(ID, 平台名称, 联系人, 联系电话, 联系邮箱, 创建时间, 操作)
    • Company 表格:7列(公司名称, 平台, 联系人, 联系电话, 状态, 创建时间, 操作)
    • Company 显示关联的平台名称和状态徽章
  4. 删除限制:

    • 只有禁用状态(status=0)的公司才能删除
    • 测试中需要先禁用公司再删除

测试数据唯一性策略

使用时间戳确保唯一性:

const timestamp = Date.now();
const uniqueId = `company_${timestamp}`;
const companyName = `测试公司_${uniqueId}`;

测试数据清理:

  • 每个测试用例结束后清理自己创建的公司数据
  • 使用 deleteCompany() 方法删除测试数据
  • 验证删除成功后再结束测试

依赖关系

Epic 11 内部依赖:

  • Story 11.1: ✅ 已完成(PlatformManagementPage 作为参考)
  • Story 11.2: ✅ 已完成(平台创建测试参考)
  • Story 11.3: ✅ 已完成(平台列表测试参考)
  • Story 11.4: Company 管理 Page Object(当前)

外部依赖:

  • Epic 1, 2: @d8d/e2e-test-utils 包(已存在)
  • web/tests/e2e/utils/test-setup.ts: 需要添加 companyManagementPage fixture

测试夹具配置(需要在 test-setup.ts 中添加):

export const test = test.extend<{
  adminLoginPage: AdminLoginPage;
  companyManagementPage: CompanyManagementPage;
}>({
  adminLoginPage: async ({ page }, use) => {
    await use(new AdminLoginPage(page));
  },
  companyManagementPage: async ({ page }, use) => {
    await use(new CompanyManagementPage(page));
  },
});

测试标准和规范

遵循项目测试标准:

  • docs/standards/testing-standards.md
  • docs/standards/web-ui-testing-standards.md

关键测试原则:

  1. 测试独立性:每个测试用例独立运行
  2. 数据清理:每个测试结束后清理自己创建的数据
  3. 清晰断言:使用 expect() 明确断言预期结果
  4. 等待策略:使用 Playwright 的 auto-waiting

前序 Story (11.1-11.3) 关键经验

从 Story 11.1-11.3 中学到的关键经验:

  1. Toast 检测不可靠:

    • Toast 消息有时出现得很快
    • 已改用 API 响应验证作为主要验证方式
  2. 表格列顺序:

    • Company 表格列顺序:公司名称(0), 平台(1), 联系人(2), 联系电话(3), 状态(4), 创建时间(5), 操作(6)
    • 使用 nth(0) 检查公司名称列
  3. 测试数据要求:

    • 后端 Zod schema 要求 contactEmail 必须是有效邮箱(如果填写)
    • 空字符串会被拒绝,所以测试不填的字段应该设为 undefined
  4. 页面刷新:

    • 创建/删除数据后需要刷新页面或重新导航
    • 使用 page.reload()goto() 刷新
  5. 选择器优先级:

    • 优先使用 data-testid(最稳定)
    • 其次使用 role + name(较稳定)
    • 避免使用文本选择器(可能变化)

已知问题和注意事项

  1. Platform 异步加载:

    • PlatformSelector 选项是异步加载的
    • 需要等待选项加载完成再选择
    • 使用 selectRadixOptionAsync 可能更可靠
  2. 公司名称唯一性:

    • 同一平台下公司名称唯一
    • 测试使用时间戳确保唯一性
    • 如果不选择平台,公司名称全局唯一
  3. 删除限制:

    • 只有禁用状态的公司才能删除
    • 测试删除功能需要先禁用公司
    • 或者使用 API 直接删除
  4. 状态徽章显示:

    • 状态使用徽章显示:启用(绿色)、禁用(灰色)
    • 测试中可以通过颜色或文本验证状态

开发顺序建议

  1. 创建文件和基础结构
  2. 定义所有选择器(参考 CompanyManagement.tsx 的 data-testid)
  3. 实现导航和基础验证方法
  4. 实现对话框操作方法
  5. 实现表单填写方法(包括 PlatformSelector 选择)
  6. 实现表单提交方法(网络响应捕获)
  7. 实现 CRUD 操作方法
  8. 实现搜索和验证方法
  9. TypeScript 类型检查
  10. 在 test-setup.ts 中添加 fixture

References

Project Structure Notes

测试文件存放路径:

web/tests/e2e/
├── pages/admin/
│   ├── platform-management.page.ts   # 平台管理 Page Object(已完成)
│   └── company-management.page.ts    # 公司管理 Page Object(当前)
├── specs/admin/
│   ├── platform-create.spec.ts       # 平台创建测试(已完成)
│   ├── platform-list.spec.ts         # 平台列表测试(已完成)
│   └── company-*.spec.ts             # 公司管理测试(后续 Story)
└── utils/
    └── test-setup.ts                  # 测试夹具配置(需要添加 companyManagementPage)

Dev Agent Record

Agent Model Used

Claude (d8d-model)

Completion Notes List

Story 创建完成:

  1. ✅ 分析 Story 11.4 需求:创建 CompanyManagementPage Page Object
  2. ✅ 创建完整的 Story 文档
  3. ✅ 包含所有验收标准和任务分解
  4. ✅ 提供详细的 Dev Notes 指导开发者
  5. ✅ 参考 Story 11.1-11.3 的关键经验和模式
  6. ✅ 分析 CompanyManagement.tsx UI 组件结构
  7. ✅ 提供完整的 Page Object 类设计参考
  8. ✅ 列出所有 data-testid 选择器
  9. ✅ 说明 PlatformSelector 集成方法

Story 实施完成 (2026-01-12):

  1. ✅ 创建 web/tests/e2e/pages/admin/company-management.page.ts 文件
  2. ✅ 定义完整的类结构和选择器
  3. ✅ 实现所有导航和基础验证方法
  4. ✅ 实现所有对话框操作方法
  5. ✅ 实现表单填写和提交方法(含网络响应捕获)
  6. ✅ 实现 CRUD 操作方法(创建、编辑、删除)
  7. ✅ 实现搜索和验证方法
  8. ✅ 添加 companyManagementPage fixture 到 test-setup.ts
  9. ✅ TypeScript 类型检查通过(无类型错误)

代码审查完成 (2026-01-12):

  1. ✅ 集成 @d8d/e2e-test-utilsselectRadixOptionAsync 实现 PlatformSelector 选择
  2. ✅ 优化删除方法中的 token 查找策略(仅检查标准键名)
  3. ✅ 清理过多的 console.debug 语句(保留关键错误日志)
  4. ✅ 提取硬编码的 API URL 为类常量
  5. ✅ 更新 fillCompanyFormcreateCompanyeditCompany 方法签名,添加 platformName 参数

Story 文件位置:

  • _bmad-output/implementation-artifacts/11-4-company-page-object.story.md

实现的关键特性:

  • 遵循 PlatformManagementPage 的设计模式
  • 使用 data-testid 优先的选择器策略
  • 支持 Toast 消息验证和 API 响应捕获
  • API 直接删除策略(绕过 UI 不稳定性)
  • 完整的 JSDoc 注释
  • 导出 CompanyStatus 常量和类型
  • 集成 @d8d/e2e-test-utils 实现 PlatformSelector 选择
  • API URL 配置化为类常量

File List

新创建的文件(Story 创建):

  • _bmad-output/implementation-artifacts/11-4-company-page-object.story.md

新创建的文件(Story 实施):

  • web/tests/e2e/pages/admin/company-management.page.ts

修改的文件(Story 实施):

  • web/tests/e2e/utils/test-setup.ts - 添加 companyManagementPage fixture

已有的依赖文件:

  • web/tests/e2e/pages/admin/platform-management.page.ts - Story 11.1 创建(参考模式)
  • allin-packages/company-management-ui/src/components/CompanyManagement.tsx - UI 组件参考
  • allin-packages/company-module/src/entities/company.entity.ts - 实体参考
  • allin-packages/company-module/src/schemas/company.schema.ts - Schema 参考