import { Page, Locator } from '@playwright/test'; import { selectRadixOption } from '@d8d/e2e-test-utils'; /** * 订单状态常量 */ export const ORDER_STATUS = { DRAFT: 'draft', CONFIRMED: 'confirmed', IN_PROGRESS: 'in_progress', COMPLETED: 'completed', } as const; /** * 订单状态类型 */ export type OrderStatus = typeof ORDER_STATUS[keyof typeof ORDER_STATUS]; /** * 订单状态显示名称映射 */ export const ORDER_STATUS_LABELS: Record = { draft: '草稿', confirmed: '已确认', in_progress: '进行中', completed: '已完成', } as const; /** * 工作状态常量 */ export const WORK_STATUS = { NOT_EMPLOYED: 'not_employed', PENDING: 'pending', EMPLOYED: 'employed', RESIGNED: 'resigned', } as const; /** * 工作状态类型 */ export type WorkStatus = typeof WORK_STATUS[keyof typeof WORK_STATUS]; /** * 工作状态显示名称映射 */ export const WORK_STATUS_LABELS: Record = { not_employed: '未就业', pending: '待就业', employed: '已就业', resigned: '已离职', } as const; /** * 订单数据接口 */ export interface OrderData { /** 订单名称 */ name: string; /** 预计开始日期 */ expectedStartDate?: string; /** 平台ID */ platformId?: number; /** 平台名称 */ platformName?: string; /** 公司ID */ companyId?: number; /** 公司名称 */ companyName?: string; /** 渠道ID */ channelId?: number; /** 渠道名称 */ channelName?: string; /** 订单状态 */ status?: OrderStatus; /** 工作状态 */ workStatus?: WorkStatus; } /** * 订单人员数据接口 */ export interface OrderPersonData { /** 残疾人ID */ disabledPersonId: number; /** 残疾人姓名 */ disabledPersonName?: string; /** 入职日期 */ hireDate?: string; /** 薪资 */ salary?: number; /** 工作状态 */ workStatus?: WorkStatus; /** 实际入职日期 */ actualHireDate?: string; /** 离职日期 */ resignDate?: string; } /** * 网络响应数据接口 */ export interface NetworkResponse { /** 请求URL */ url: string; /** 请求方法 */ method: string; /** 响应状态码 */ status: number; /** 是否成功 */ ok: boolean; /** 响应头 */ responseHeaders: Record; /** 响应体 */ responseBody: unknown; } /** * 表单提交结果接口 */ export interface FormSubmitResult { /** 提交是否成功 */ success: boolean; /** 是否有错误 */ hasError: boolean; /** 是否有成功消息 */ hasSuccess: boolean; /** 错误消息 */ errorMessage?: string; /** 成功消息 */ successMessage?: string; /** 网络响应列表 */ responses?: NetworkResponse[]; } /** * 订单管理 Page Object * * 用于订单管理功能的 E2E 测试 * 页面路径: /admin/orders(待确认) * * @example * ```typescript * const orderPage = new OrderManagementPage(page); * await orderPage.goto(); * await orderPage.createOrder({ name: '测试订单' }); * ``` */ export class OrderManagementPage { readonly page: Page; // ===== 页面级选择器 ===== /** 页面标题 */ readonly pageTitle: Locator; /** 新增订单按钮 */ readonly addOrderButton: Locator; /** 订单列表表格 */ readonly orderTable: Locator; /** 搜索输入框 */ readonly searchInput: Locator; /** 搜索按钮 */ readonly searchButton: Locator; constructor(page: Page) { this.page = page; // 初始化页面级选择器 // 使用更精确的选择器来定位页面标题(避免与侧边栏按钮冲突) this.pageTitle = page.locator('[data-slot="card-title"]').getByText('订单管理', { exact: true }); // 使用 data-testid 定位创建订单按钮(按钮文本是"创建订单"不是"新增订单") this.addOrderButton = page.getByTestId('create-order-button'); this.orderTable = page.locator('table'); // 使用 data-testid 定位搜索输入框 this.searchInput = page.getByTestId('search-order-name-input'); // 使用 data-testid 定位搜索按钮 this.searchButton = page.getByTestId('search-button'); } // ===== 导航和基础验证 ===== /** * 导航到订单管理页面 */ async goto() { await this.page.goto('/admin/orders'); await this.page.waitForLoadState('domcontentloaded'); // 等待页面标题出现 await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 }); // 等待表格数据加载 await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: 20000 }); await this.expectToBeVisible(); } /** * 验证页面关键元素可见 */ async expectToBeVisible() { await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 }); await this.addOrderButton.waitFor({ state: 'visible', timeout: 10000 }); } // ===== 搜索和筛选功能 ===== /** * 按订单名称搜索 * @param name 订单名称 */ async searchByName(name: string) { await this.searchInput.fill(name); await this.searchButton.click(); await this.page.waitForLoadState('networkidle'); await this.page.waitForTimeout(1000); } /** * 打开高级筛选对话框 */ async openFilterDialog() { const filterButton = this.page.getByRole('button', { name: /筛选|高级筛选/ }); await filterButton.click(); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 }); } /** * 设置筛选条件 * @param filters 筛选条件 */ async setFilters(filters: { status?: OrderStatus; workStatus?: WorkStatus; platformId?: number; platformName?: string; companyId?: number; companyName?: string; channelId?: number; channelName?: string; dateRange?: { start?: string; end?: string }; }) { // 订单状态筛选 if (filters.status) { const statusFilter = this.page.getByLabel(/订单状态/); await statusFilter.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(); } // 平台筛选 if (filters.platformName) { await selectRadixOption(this.page, '平台', filters.platformName); } // 公司筛选 if (filters.companyName) { await selectRadixOption(this.page, '公司', filters.companyName); } // 渠道筛选 if (filters.channelName) { await selectRadixOption(this.page, '渠道', filters.channelName); } // 日期范围筛选 if (filters.dateRange) { if (filters.dateRange.start) { const startDateInput = this.page.getByLabel(/开始日期|起始日期/); await startDateInput.fill(filters.dateRange.start); } if (filters.dateRange.end) { const endDateInput = this.page.getByLabel(/结束日期|截止日期/); await endDateInput.fill(filters.dateRange.end); } } } /** * 应用筛选条件 */ async applyFilters() { const applyButton = this.page.getByRole('button', { name: /应用|确定|筛选/ }); await applyButton.click(); await this.page.waitForLoadState('networkidle'); await this.page.waitForTimeout(1000); } /** * 清空筛选条件 */ async clearFilters() { const clearButton = this.page.getByRole('button', { name: /重置|清空/ }); await clearButton.click(); await this.page.waitForTimeout(500); } // ===== 订单 CRUD 操作 ===== /** * 打开创建订单对话框 */ async openCreateDialog() { await this.addOrderButton.click(); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 }); } /** * 打开编辑订单对话框 * @param orderName 订单名称 */ async openEditDialog(orderName: string) { // 找到订单行并点击"打开菜单"按钮 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); await menuButton.click(); // 等待菜单出现并点击"编辑"选项 // 使用 data-testid 或 role 定位编辑选项 const editOption = this.page.getByRole('menuitem', { name: '编辑' }); await editOption.waitFor({ state: 'visible', timeout: 3000 }); await editOption.click(); // 等待编辑对话框出现 await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 }); } /** * 打开删除确认对话框 * @param orderName 订单名称 */ async openDeleteDialog(orderName: string) { // 找到订单行并点击"打开菜单"按钮(与编辑操作相同的模式) const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); await menuButton.click(); // 等待菜单出现并点击"删除"选项 const deleteOption = this.page.getByRole('menuitem', { name: '删除' }); await deleteOption.waitFor({ state: 'visible', timeout: 3000 }); await deleteOption.click(); // 等待删除确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 }); } /** * 填写订单表单 * @param data 订单数据 */ async fillOrderForm(data: OrderData) { // 等待表单出现 await this.page.waitForSelector('form', { state: 'visible', timeout: 5000 }); // 填写订单名称 if (data.name) { await this.page.getByLabel(/订单名称|名称/).fill(data.name); } // 填写预计开始日期 if (data.expectedStartDate) { const dateInput = this.page.getByLabel(/预计开始日期|开始日期/); await dateInput.fill(data.expectedStartDate); } // 选择平台 if (data.platformName) { await selectRadixOption(this.page, '平台', data.platformName); } // 选择公司 if (data.companyName) { await selectRadixOption(this.page, '公司', data.companyName); } // 选择渠道 if (data.channelName) { await selectRadixOption(this.page, '渠道', data.channelName); } // 选择订单状态(如果是编辑模式) if (data.status) { const statusLabel = ORDER_STATUS_LABELS[data.status]; await selectRadixOption(this.page, '订单状态', statusLabel); } // 选择工作状态(如果是编辑模式) if (data.workStatus) { const workStatusLabel = WORK_STATUS_LABELS[data.workStatus]; await selectRadixOption(this.page, '工作状态', workStatusLabel); } } /** * 提交表单 * @returns 表单提交结果 */ async submitForm(): Promise { // 收集网络响应 const responses: NetworkResponse[] = []; // 监听所有网络请求 const responseHandler = async (response: Response) => { const url = response.url(); // 监听订单管理相关的 API 请求 if (url.includes('/orders') || url.includes('order')) { const requestBody = response.request()?.postData(); const responseBody = await response.text().catch(() => ''); let jsonBody = null; try { jsonBody = JSON.parse(responseBody); } catch { // 不是 JSON 响应 } responses.push({ url, method: response.request()?.method() ?? 'UNKNOWN', status: response.status(), ok: response.ok(), responseHeaders: await response.allHeaders().catch(() => ({})), responseBody: jsonBody || responseBody, }); } }; this.page.on('response', responseHandler); try { // 点击提交按钮(创建或更新) const submitButton = this.page.getByRole('button', { name: /^(创建|更新|保存)$/ }); await submitButton.click(); // 等待网络请求完成(使用较宽松的超时,因为有些操作可能不触发网络请求) try { await this.page.waitForLoadState('networkidle', { timeout: 5000 }); } catch { // networkidle 超时不是致命错误,继续检查 Toast 消息 console.debug('networkidle 超时,继续检查 Toast 消息'); } } finally { // 确保监听器总是被移除,防止内存泄漏 this.page.off('response', responseHandler); } // 等待 Toast 消息显示 await this.page.waitForTimeout(2000); // 检查 Toast 消息 const errorToast = this.page.locator('[data-sonner-toast][data-type="error"]'); const successToast = this.page.locator('[data-sonner-toast][data-type="success"]'); const hasError = await errorToast.count() > 0; const hasSuccess = await successToast.count() > 0; let errorMessage: string | null = null; let successMessage: string | null = null; if (hasError) { errorMessage = await errorToast.first().textContent(); } if (hasSuccess) { successMessage = await successToast.first().textContent(); } return { success: hasSuccess || (!hasError && !hasSuccess), hasError, hasSuccess, errorMessage: errorMessage ?? undefined, successMessage: successMessage ?? undefined, responses, }; } /** * 取消对话框 */ async cancelDialog() { const cancelButton = this.page.getByRole('button', { name: '取消' }); await cancelButton.click(); await this.waitForDialogClosed(); } /** * 等待对话框关闭 */ async waitForDialogClosed() { const dialog = this.page.locator('[role="dialog"]'); await dialog.waitFor({ state: 'hidden', timeout: 5000 }) .catch(() => console.debug('对话框关闭超时,可能已经关闭')); await this.page.waitForTimeout(500); } /** * 确认删除操作 */ async confirmDelete() { // 尝试多种可能的按钮名称 const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: /^(确认删除|删除|确定|确认)$/ }); await confirmButton.click(); // 等待确认对话框关闭和网络请求完成 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); } /** * 取消删除操作 */ async cancelDelete() { const cancelButton = this.page.getByRole('button', { name: '取消' }).and( this.page.locator('[role="alertdialog"]') ); await cancelButton.click(); await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }) .catch(() => console.debug('删除确认对话框关闭超时(取消操作)')); } /** * 验证订单是否存在 * @param orderName 订单名称 * @returns 订单是否存在 */ async orderExists(orderName: string): Promise { const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); return (await orderRow.count()) > 0; } // ===== 订单详情 ===== /** * 打开订单详情对话框 * @param orderName 订单名称 */ async openDetailDialog(orderName: string) { // 找到订单行并点击查看详情按钮 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); const detailButton = orderRow.getByRole('button', { name: /详情|查看/ }); await detailButton.click(); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 }); } /** * 获取订单详情中的基本信息 * @returns 订单基本信息 */ async getOrderDetailInfo(): Promise<{ name?: string; status?: string; workStatus?: string; expectedStartDate?: string; platform?: string; company?: string; channel?: string; }> { const dialog = this.page.locator('[role="dialog"]'); const result: Record = {}; // 订单名称 - 查找"订单名称"标签后的值 const nameElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单名称' }) .locator('..').locator('p,span,div').nth(1); if (await nameElement.count() > 0) { const text = await nameElement.textContent(); result.name = text || undefined; } // 订单状态 const statusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单状态' }) .locator('..').locator('p,span,div').nth(1); if (await statusElement.count() > 0) { const text = await statusElement.textContent(); result.status = text || undefined; } // 工作状态 const workStatusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '工作状态' }) .locator('..').locator('p,span,div').nth(1); if (await workStatusElement.count() > 0) { 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; } // ===== 人员关联管理 ===== /** * 打开人员管理对话框 * * **使用场景:** * - **从订单列表页打开**: 传入 `orderName` 参数,方法会先找到对应订单行,再点击人员管理按钮 * - **从订单详情页打开**: 不传参数,方法会直接点击页面中的人员管理按钮 * * @param orderName 订单名称(可选)。从列表页打开时需要传入,从详情页打开时不传 * * @example * ```typescript * // 从订单列表页打开 * await orderPage.openPersonManagementDialog('测试订单'); * * // 从订单详情页打开 * await orderPage.openDetailDialog('测试订单'); * await orderPage.openPersonManagementDialog(); * ``` */ async openPersonManagementDialog(orderName?: string) { // 如果提供了订单名称,先找到对应的订单行 if (orderName) { const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); const personButton = orderRow.getByRole('button', { name: /人员|员工/ }); await personButton.click(); } else { // 如果在详情页,直接点击人员管理按钮 const personButton = this.page.getByRole('button', { name: /人员管理|添加人员/ }); await personButton.click(); } await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 }); } /** * 添加人员到订单 * @param personData 人员数据 */ async addPersonToOrder(personData: OrderPersonData) { // 点击添加人员按钮 const addButton = this.page.getByRole('button', { name: /添加人员|新增人员/ }); await addButton.click(); await this.page.waitForTimeout(300); // 选择残疾人 if (personData.disabledPersonName) { await selectRadixOption(this.page, '残疾人|选择残疾人', personData.disabledPersonName); } // 填写入职日期 if (personData.hireDate) { const hireDateInput = this.page.getByLabel(/入职日期/); await hireDateInput.fill(personData.hireDate); } // 填写薪资 if (personData.salary !== undefined) { const salaryInput = this.page.getByLabel(/薪资|工资/); await salaryInput.fill(String(personData.salary)); } // 选择工作状态 if (personData.workStatus) { const workStatusLabel = WORK_STATUS_LABELS[personData.workStatus]; await selectRadixOption(this.page, '工作状态', workStatusLabel); } // 提交 const submitButton = this.page.getByRole('button', { name: /^(添加|确定|保存)$/ }); await submitButton.click(); await this.page.waitForLoadState('networkidle'); await this.page.waitForTimeout(1000); } /** * 修改人员工作状态 * @param personName 人员姓名 * @param newStatus 新的工作状态 */ async updatePersonWorkStatus(personName: string, newStatus: WorkStatus) { // 找到人员行 const personRow = this.page.locator('[role="dialog"]').locator('table tbody tr').filter({ hasText: personName }); // 点击编辑工作状态按钮 const editButton = personRow.getByRole('button', { name: /编辑|修改/ }); await editButton.click(); await this.page.waitForTimeout(300); // 选择新的工作状态 const workStatusLabel = WORK_STATUS_LABELS[newStatus]; await selectRadixOption(this.page, '工作状态', workStatusLabel); // 提交 const submitButton = this.page.getByRole('button', { name: /^(更新|保存|确定)$/ }); await submitButton.click(); await this.page.waitForLoadState('networkidle'); await this.page.waitForTimeout(1000); } // ===== 附件管理 ===== /** * 打开添加附件对话框 */ async openAddAttachmentDialog() { const attachmentButton = this.page.getByRole('button', { name: /添加附件|上传附件/ }); await attachmentButton.click(); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 }); } /** * 上传附件 * @param personName 人员姓名 * @param fileName 文件名 * @param mimeType 文件类型(默认为 image/jpeg) */ async uploadAttachment(personName: string, fileName: string, mimeType: string = 'image/jpeg') { // 选择订单人员 const personSelect = this.page.getByLabel(/选择人员|订单人员/); await personSelect.click(); await this.page.getByRole('option', { name: personName }).click(); // 查找文件上传输入框 const fileInput = this.page.locator('input[type="file"]'); await fileInput.setInputFiles({ name: fileName, mimeType, buffer: Buffer.from(`fake ${fileName} content`), }); // 等待上传处理 await this.page.waitForTimeout(500); // 提交 const submitButton = this.page.getByRole('button', { name: /^(上传|确定|保存)$/ }); await submitButton.click(); await this.page.waitForLoadState('networkidle'); await this.page.waitForTimeout(1000); } // ===== 高级操作 ===== /** * 创建订单(完整流程) * @param data 订单数据 * @returns 表单提交结果 */ async createOrder(data: OrderData): Promise { await this.openCreateDialog(); await this.fillOrderForm(data); const result = await this.submitForm(); await this.waitForDialogClosed(); return result; } /** * 编辑订单(完整流程) * @param orderName 订单名称 * @param data 更新的订单数据 * @returns 表单提交结果 */ async editOrder(orderName: string, data: OrderData): Promise { await this.openEditDialog(orderName); await this.fillOrderForm(data); const result = await this.submitForm(); await this.waitForDialogClosed(); return result; } /** * 删除订单(完整流程) * @param orderName 订单名称 * @returns 是否成功删除 */ async deleteOrder(orderName: string): Promise { await this.openDeleteDialog(orderName); await this.confirmDelete(); // 等待并检查 Toast 消息 await this.page.waitForTimeout(1000); const successToast = this.page.locator('[data-sonner-toast][data-type="success"]'); const hasSuccess = await successToast.count() > 0; return hasSuccess; } }