|
@@ -0,0 +1,632 @@
|
|
|
|
|
+# Story 11.4: Company 管理 Page Object(重点)
|
|
|
|
|
+
|
|
|
|
|
+Status: ready-for-dev
|
|
|
|
|
+
|
|
|
|
|
+<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
|
|
|
|
|
+
|
|
|
|
|
+## 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
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 1: 创建文件和基础结构 (AC: 1, 8, 9)
|
|
|
|
|
+ - [ ] 创建文件 `web/tests/e2e/pages/admin/company-management.page.ts`
|
|
|
|
|
+ - [ ] 导入 Playwright 类型和依赖
|
|
|
|
|
+ - [ ] 定义类结构:`export class CompanyManagementPage`
|
|
|
|
|
+ - [ ] 定义接口:CompanyData, NetworkResponse, FormSubmitResult
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 2: 定义页面选择器 (AC: 2, 9)
|
|
|
|
|
+ - [ ] 定义页面级选择器(pageTitle, createCompanyButton, searchInput, searchButton, companyTable)
|
|
|
|
|
+ - [ ] 定义对话框选择器(createDialogTitle, editDialogTitle)
|
|
|
|
|
+ - [ ] 定义表单字段选择器(使用 data-testid)
|
|
|
|
|
+ - [ ] 定义按钮选择器(createSubmitButton, updateSubmitButton, cancelButton, confirmDeleteButton)
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 3: 实现导航和基础验证方法 (AC: 3, 9)
|
|
|
|
|
+ - [ ] 实现 `goto()` 方法
|
|
|
|
|
+ - [ ] 实现 `expectToBeVisible()` 方法
|
|
|
|
|
+ - [ ] 添加 JSDoc 注释
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 4: 实现对话框操作方法 (AC: 4, 9)
|
|
|
|
|
+ - [ ] 实现 `openCreateDialog()` 方法
|
|
|
|
|
+ - [ ] 实现 `openEditDialog(companyName)` 方法
|
|
|
|
|
+ - [ ] 实现 `openDeleteDialog(companyName)` 方法
|
|
|
|
|
+ - [ ] 实现 `cancelDialog()` 和 `waitForDialogClosed()` 方法
|
|
|
|
|
+ - [ ] 添加 JSDoc 注释
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 5: 实现表单操作方法 (AC: 5, 9)
|
|
|
|
|
+ - [ ] 实现 `fillCompanyForm(data)` 方法
|
|
|
|
|
+ - [ ] 实现 `submitForm()` 方法(包含网络响应捕获)
|
|
|
|
|
+ - [ ] 处理 Toast 消息验证
|
|
|
|
|
+ - [ ] 添加 JSDoc 注释
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 6: 实现 CRUD 操作方法 (AC: 6, 9)
|
|
|
|
|
+ - [ ] 实现 `createCompany(data)` 方法
|
|
|
|
|
+ - [ ] 实现 `editCompany(companyName, data)` 方法
|
|
|
|
|
+ - [ ] 实现 `deleteCompany(companyName)` 方法(API 直接删除)
|
|
|
|
|
+ - [ ] 添加 JSDoc 注释
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 7: 实现搜索和验证方法 (AC: 7, 9)
|
|
|
|
|
+ - [ ] 实现 `searchByName(name)` 方法
|
|
|
|
|
+ - [ ] 实现 `companyExists(companyName)` 方法
|
|
|
|
|
+ - [ ] 添加 JSDoc 注释
|
|
|
|
|
+
|
|
|
|
|
+- [ ] 任务 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 响应
|
|
|
|
|
+ - 监听 `createCompany`、`updateCompany`、`getAllCompanies` 等 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):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+{
|
|
|
|
|
+ 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-utils` 的 `selectRadixOption` 或 `selectRadixOptionAsync`
|
|
|
|
|
+ - 标签:"平台" 或 "选择平台"
|
|
|
|
|
+ - 选项是异步加载的(从平台列表 API)
|
|
|
|
|
+
|
|
|
|
|
+### API 端点参考
|
|
|
|
|
+
|
|
|
|
|
+**公司管理 API 端点:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 获取所有公司
|
|
|
|
|
+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 类结构:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+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 工具:**
|
|
|
|
|
+ ```typescript
|
|
|
|
|
+ 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)的公司才能删除
|
|
|
|
|
+ - 测试中需要先禁用公司再删除
|
|
|
|
|
+
|
|
|
|
|
+### 测试数据唯一性策略
|
|
|
|
|
+
|
|
|
|
|
+**使用时间戳确保唯一性:**
|
|
|
|
|
+```typescript
|
|
|
|
|
+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 中添加):**
|
|
|
|
|
+```typescript
|
|
|
|
|
+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
|
|
|
|
|
+
|
|
|
|
|
+- [Epic 11 基础配置管理测试](../planning-artifacts/epics.md#epic-11-基础配置管理测试-epic-f)
|
|
|
|
|
+- [Story 11.1 Platform Page Object](./11-1-platform-page-object.story.md)
|
|
|
|
|
+- [Story 11.2 平台创建测试](./11-2-platform-create-test.story.md)
|
|
|
|
|
+- [Story 11.3 平台列表测试](./11-3-platform-list-test.story.md)
|
|
|
|
|
+- [Company Entity](../../allin-packages/company-module/src/entities/company.entity.ts)
|
|
|
|
|
+- [Company Schema](../../allin-packages/company-module/src/schemas/company.schema.ts)
|
|
|
|
|
+- [CompanyManagement UI](../../allin-packages/company-management-ui/src/components/CompanyManagement.tsx)
|
|
|
|
|
+- [PlatformManagementPage](../../web/tests/e2e/pages/admin/platform-management.page.ts) - 参考模式
|
|
|
|
|
+- [测试标准](../../docs/standards/testing-standards.md)
|
|
|
|
|
+- [Web UI 测试标准](../../docs/standards/web-ui-testing-standards.md)
|
|
|
|
|
+- [Architecture Decision - TypeScript + Playwright 陷阱](../planning-artifacts/architecture.md#typescript--playwright-常见陷阱)
|
|
|
|
|
+
|
|
|
|
|
+### 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 文件位置:**
|
|
|
|
|
+- `_bmad-output/implementation-artifacts/11-4-company-page-object.story.md`
|
|
|
|
|
+
|
|
|
|
|
+### 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 参考
|