Status: done
作为测试开发者, 我想要创建渠道管理的 Page Object(可选), 以便为渠道管理功能的 E2E 测试封装页面元素和操作方法(如需要)。
AC1: 创建 ChannelManagementPage 文件(可选)
web/tests/e2e/pages/admin/channel-management.page.tsAC2: 定义页面选择器
AC3: 实现导航和基础验证方法
goto() 方法:导航到渠道管理页面expectToBeVisible() 方法:验证页面关键元素可见AC4: 实现对话框操作方法
openCreateDialog() 方法:打开创建渠道对话框openEditDialog(channelName) 方法:打开编辑渠道对话框openDeleteDialog(channelName) 方法:打开删除确认对话框cancelDialog() 方法:取消对话框waitForDialogClosed() 方法:等待对话框关闭AC5: 实现表单操作方法
fillChannelForm(data) 方法:填写渠道表单submitForm() 方法:提交表单并捕获网络响应AC6: 实现 CRUD 操作方法
createChannel(data) 方法:创建渠道(完整流程)editChannel(channelName, data) 方法:编辑渠道(完整流程)deleteChannel(channelName) 方法:删除渠道(使用 API 直接删除)AC7: 实现搜索和验证方法
searchByName(name) 方法:按渠道名称搜索channelExists(channelName) 方法:验证渠道是否存在(使用精确匹配)AC8: 定义数据接口和类型
AC9: 代码质量标准
[x] 任务 1: 创建文件和基础结构 (AC: 1, 8, 9)
web/tests/e2e/pages/admin/channel-management.page.tsexport class ChannelManagementPage[x] 任务 2: 定义页面选择器 (AC: 2, 9)
[x] 任务 3: 实现导航和基础验证方法 (AC: 3, 9)
goto() 方法expectToBeVisible() 方法[x] 任务 4: 实现对话框操作方法 (AC: 4, 9)
openCreateDialog() 方法openEditDialog(channelName) 方法openDeleteDialog(channelName) 方法cancelDialog() 和 waitForDialogClosed() 方法[x] 任务 5: 实现表单操作方法 (AC: 5, 9)
fillChannelForm(data) 方法submitForm() 方法(包含网络响应捕获)[x] 任务 6: 实现 CRUD 操作方法 (AC: 6, 9)
createChannel(data) 方法editChannel(channelName, data) 方法deleteChannel(channelName) 方法(API 直接删除)[x] 任务 7: 实现搜索和验证方法 (AC: 7, 9)
searchByName(name) 方法channelExists(channelName) 方法[x] 任务 8: TypeScript 类型检查和代码质量验证 (AC: 9)
pnpm typecheck 检查类型Epic 11: 基础配置管理测试 (Epic F)
为平台、公司、渠道配置管理编写 E2E 测试,为后续用户管理和跨端测试提供必要的测试数据。
实体关系链:
Platform (平台)
↓ 1:N
Company (公司) - 必须 platformId
↓ 1:N
Order (订单) - 必须 companyId
↓
Channel (渠道) - 订单的可选条件
Story 11.7 可选性的原因:
Channel 是订单的可选字段
channelId 是可选的(optional())优先级较低
实现时机
Channel 实体关键特点:
idx_channel_name)参考 Story 11.1 (Platform) 和 Story 11.4 (Company) 的 Page Object 模式:
选择器策略:
data-testid 定位元素网络响应捕获:
waitForResponse() 捕获特定 API 响应createChannel、updateChannel、getAllChannels 等 APIPromise.race() 添加超时保护Toast 消息验证:
[data-sonner-toast][data-type="error"] 和 [data-type="success"]删除操作策略:
page.evaluate() 在浏览器上下文中执行 fetchChannel Entity (来自 channel.entity.ts):
{
id: number; // 主键ID (channel_id)
channelName: string; // 渠道名称(必填,最大100字符,全局唯一)
channelType: string; // 渠道类型(必填,最大50字符,默认空字符串)
contactPerson: string; // 联系人(必填,最大50字符,默认空字符串)
contactPhone: string; // 联系电话(必填,最大20字符,默认空字符串)
description?: string; // 描述(可选,text类型)
status: number; // 状态:1-正常,0-禁用(默认1)
createTime: Date; // 创建时间
updateTime: Date; // 更新时间
}
唯一约束:
idx_channel_name (channelName)与 Company/Platform 的差异:
页面路径: 待确认(可能是 /admin/channels)
UI 组件结构(来自 ChannelManagement.tsx):
页面结构:
创建/编辑对话框:
删除确认对话框:
data-testid 属性(关键):
搜索相关:
- search-input (搜索输入框)
- search-button (搜索按钮)
- create-channel-button (创建渠道按钮)
表单字段(创建):
- create-channel-modal-title (对话框标题)
表单字段(编辑):
- (编辑表单使用 react-hook-form,未显式设置 data-testid)
删除对话框:
- delete-confirm-dialog-title (删除确认对话框标题)
表格操作按钮:
- edit-channel-{id} (编辑按钮,动态ID)
- delete-channel-{id} (删除按钮,动态ID)
注意事项:
渠道管理 API 端点:
// 获取所有渠道
GET /api/v1/channel/getAllChannels?skip=0&take=10
// 搜索渠道
GET /api/v1/channel/searchChannels?name=xxx&skip=0&take=10
// 创建渠道
POST /api/v1/channel/createChannel
Body: { channelName, channelType?, contactPerson?, contactPhone?, description? }
// 更新渠道
POST /api/v1/channel/updateChannel
Body: { id, channelName?, channelType?, contactPerson?, contactPhone?, description? }
// 删除渠道
POST /api/v1/channel/deleteChannel
Body: { id: number }
ChannelManagementPage 类结构:
import { Page, Locator } from '@playwright/test';
/**
* 渠道数据接口
*/
export interface ChannelData {
/** 渠道名称(必填) */
channelName: string;
/** 渠道类型(可选) */
channelType?: string;
/** 联系人(可选) */
contactPerson?: string;
/** 联系电话(可选) */
contactPhone?: string;
/** 描述(可选) */
description?: 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/channels(待确认)
*/
export class ChannelManagementPage {
readonly page: Page;
// ===== 页面级选择器 =====
readonly pageTitle: Locator;
readonly createChannelButton: Locator;
readonly searchInput: Locator;
readonly searchButton: Locator;
readonly channelTable: Locator;
// ===== 对话框选择器 =====
readonly createDialogTitle: Locator;
readonly editDialogTitle: Locator;
// ===== 表单字段选择器 =====
readonly channelNameInput: Locator;
readonly channelTypeInput: Locator;
readonly contactPersonInput: Locator;
readonly contactPhoneInput: Locator;
readonly descriptionInput: 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(channelName: string): Promise<void>
async openDeleteDialog(channelName: string): Promise<void>
async cancelDialog(): Promise<void>
async waitForDialogClosed(): Promise<void>
async confirmDelete(): Promise<void>
async cancelDelete(): Promise<void>
// ===== 表单操作 =====
async fillChannelForm(data: ChannelData): Promise<void>
async submitForm(): Promise<FormSubmitResult>
// ===== CRUD 操作 =====
async createChannel(data: ChannelData): Promise<FormSubmitResult>
async editChannel(channelName: string, data: ChannelData): Promise<FormSubmitResult>
async deleteChannel(channelName: string): Promise<boolean>
// ===== 搜索和验证 =====
async searchByName(name: string): Promise<boolean>
async channelExists(channelName: string): Promise<boolean>
}
相同部分:
差异部分:
| 特性 | Platform | Company | Channel |
|---|---|---|---|
| 外键依赖 | 无 | 需要 platformId | 无 |
| 名称唯一性 | 全局 | 同平台下唯一 | 全局 |
| 表格列数 | 7列 | 7列 | 7列 |
| 状态显示 | 无 | 有(状态徽章) | 有(状态字段) |
| 必填字段 | platformName | companyName | channelName |
| 可选字段 | 联系人信息 | 地址 + platformId | channelType, description |
Channel 特有的特点:
使用时间戳确保唯一性:
const timestamp = Date.now();
const uniqueId = `channel_${timestamp}`;
const channelName = `测试渠道_${uniqueId}`;
测试数据清理:
deleteChannel() 方法删除测试数据Epic 11 内部依赖:
外部依赖:
@d8d/e2e-test-utils 包(已存在)web/tests/e2e/utils/test-setup.ts: 需要添加 channelManagementPage fixture测试夹具配置(需要在 test-setup.ts 中添加):
export const test = test.extend<{
adminLoginPage: AdminLoginPage;
channelManagementPage: ChannelManagementPage;
}>({
adminLoginPage: async ({ page }, use) => {
await use(new AdminLoginPage(page));
},
channelManagementPage: async ({ page }, use) => {
await use(new ChannelManagementPage(page));
},
});
遵循项目测试标准:
docs/standards/testing-standards.mddocs/standards/web-ui-testing-standards.md关键测试原则:
从 Story 11.1-11.6 中学到的关键经验:
Toast 检测不可靠:
表格列顺序:
nth(1) 检查渠道名称列测试数据要求:
页面刷新:
page.reload() 或 goto() 刷新选择器优先级:
编辑表单 data-testid 缺失:
getByRole('form') + getByLabel() 组合定位页面路由待确认:
/admin/channels 或其他data-testid 不完整:
渠道名称全局唯一:
删除限制:
状态显示:
由于此 Story 是可选的,实现前需要确认:
是否需要渠道管理测试?
优先级评估:
实现范围:
标记为"可选"的原因总结:
何时应该实现此 Story:
技术方案:
ChannelManagementPage 类,遵循 PlatformManagementPage 和 CompanyManagementPage 的相同模式data-testid 定位元素(UI 组件中已设置的部分)data-testid 的编辑表单字段,使用 role + label 组合定位实现要点:
/admin/channels(已从 routes.tsx 确认)选择器策略:
data-testid="create-channel-button"data-testid="search-input"data-testid="search-button"data-testid="create-channel-modal-title"getByText('编辑渠道')getByLabel() 定位(编辑表单未设置 data-testid)data-testid="delete-channel-{id}"(动态ID)无特殊问题需要记录。实现过程顺利,参考了 PlatformManagementPage 和 CompanyManagementPage 的成熟模式。
已完成的工作:
web/tests/e2e/pages/admin/channel-management.page.ts 文件ChannelData, NetworkResponse, FormSubmitResultgoto(), expectToBeVisible()openCreateDialog(), openEditDialog(), openDeleteDialog(), cancelDialog(), waitForDialogClosed()fillChannelForm(), submitForm()(包含网络响应捕获和 Toast 验证)createChannel(), editChannel(), deleteChannel()(API 直接删除)searchByName(), channelExists()代码质量验证:
注意事项:
data-testid,已使用 role + label 组合定位locator('..').getByRole() 组合定位,因为按钮不在直接子级中代码审查修复(2026-01-12):
新增文件:
web/tests/e2e/pages/admin/channel-management.page.ts修改文件:
_bmad-output/implementation-artifacts/sprint-status.yaml - 更新 Story 11.7 状态为 review_bmad-output/implementation-artifacts/11-7-channel-page-object.md - 本 story 文件web/tests/e2e/utils/test-setup.ts - 添加 channelManagementPage fixture注意: 工作目录中还有其他已修改的文件(如 8-9-region-stability-test.md, DisabledPersonSelector.tsx 等),这些属于其他 Story 的更改,不在本 Story 的变更范围内。
2026-01-12
测试文件存放路径:
web/tests/e2e/
├── pages/admin/
│ ├── platform-management.page.ts # 平台管理 Page Object(已完成)
│ ├── company-management.page.ts # 公司管理 Page Object(已完成)
│ └── channel-management.page.ts # 渠道管理 Page Object(当前,可选)
├── specs/admin/
│ ├── platform-create.spec.ts # 平台创建测试(已完成)
│ ├── platform-list.spec.ts # 平台列表测试(已完成)
│ ├── company-create.spec.ts # 公司创建测试(待实现)
│ ├── company-list.spec.ts # 公司列表测试(已完成)
│ └── channel-*.spec.ts # 渠道管理测试(如需要,后续实现)
└── utils/
└── test-setup.ts # 测试夹具配置(需要添加 channelManagementPage)