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('domcontentloaded', { timeout: 5000 }); } catch { // domcontentloaded 超时不是致命错误,继续检查 Toast 消息 console.debug('domcontentloaded 超时,继续检查 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() { // 先等待一段时间让对话框有机会关闭 await this.page.waitForTimeout(1000); // 检查是否还有对话框可见 const dialogs = this.page.locator('[role="dialog"]'); const dialogCount = await dialogs.count(); if (dialogCount === 0) { // 没有对话框了,已经关闭 console.debug('对话框已关闭(无对话框元素)'); return; } // 尝试等待对话框隐藏或从 DOM 中移除 try { await dialogs.first().waitFor({ state: 'hidden', timeout: 3000 }); console.debug('对话框已关闭'); } 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; } /** * 从订单详情对话框中获取关联人员列表 * @returns 人员信息列表 */ async getPersonListFromDetail(): Promise> { const dialog = this.page.locator('[role="dialog"]'); const result: Array<{ name?: string; workStatus?: string; hireDate?: string; salary?: string }> = []; // 查找人员列表区域(通常在详情对话框中有一个表格或列表展示人员) // 尝试多种可能的定位策略 const personTable = dialog.locator('table').filter({ hasText: /人员|员工/ }); const personList = dialog.locator('[class*="person"], [class*="employee"], [data-testid*="person"]'); // 优先使用表格形式 if (await personTable.count() > 0) { const rows = personTable.locator('tbody tr'); const rowCount = await rows.count(); for (let i = 0; i < rowCount; i++) { const row = rows.nth(i); const cells = row.locator('td'); const cellCount = await cells.count(); const personInfo: { name?: string; workStatus?: string; hireDate?: string; salary?: string } = {}; // 根据列数量和数据类型提取信息 for (let j = 0; j < cellCount; j++) { const cellText = await cells.nth(j).textContent(); if (!cellText) continue; const trimmedText = cellText.trim(); // 尝试识别列内容 // 姓名通常在第一列 if (j === 0 && trimmedText) { personInfo.name = trimmedText; } // 工作状态检查 for (const [statusValue, statusLabel] of Object.entries(WORK_STATUS_LABELS)) { if (trimmedText.includes(statusLabel)) { personInfo.workStatus = statusLabel; break; } } // 薪资检查(包含数字) if (/^\d+(\.\d+)?$/.test(trimmedText.replace(/,/g, ''))) { personInfo.salary = trimmedText; } // 日期检查(符合日期格式) if (/^\d{4}-\d{2}-\d{2}$/.test(trimmedText) || /^\d{4}\/\d{2}\/\d{2}$/.test(trimmedText)) { if (!personInfo.hireDate) { personInfo.hireDate = trimmedText; } } } if (personInfo.name || personInfo.workStatus) { result.push(personInfo); } } } else if (await personList.count() > 0) { // 如果是列表形式而非表格 const listItems = personList.locator('[class*="item"], [class*="row"], li, div'); const itemCount = await listItems.count(); for (let i = 0; i < itemCount; i++) { const item = listItems.nth(i); const itemText = await item.textContent(); if (itemText && itemText.trim()) { result.push({ name: itemText.trim() }); } } } return result; } /** * 从订单详情对话框中获取附件列表 * @returns 附件信息列表 */ async getAttachmentListFromDetail(): Promise> { const dialog = this.page.locator('[role="dialog"]'); const result: Array<{ fileName?: string; uploadDate?: string; uploader?: string }> = []; // 查找附件列表区域 // 尝试多种可能的定位策略 const attachmentTable = dialog.locator('table').filter({ hasText: /附件|文件/ }); const attachmentList = dialog.locator('[class*="attachment"], [class*="file"], [data-testid*="attachment"]'); // 优先使用表格形式 if (await attachmentTable.count() > 0) { const rows = attachmentTable.locator('tbody tr'); const rowCount = await rows.count(); for (let i = 0; i < rowCount; i++) { const row = rows.nth(i); const cells = row.locator('td'); const cellCount = await cells.count(); const attachmentInfo: { fileName?: string; uploadDate?: string; uploader?: string } = {}; for (let j = 0; j < cellCount; j++) { const cellText = await cells.nth(j).textContent(); if (!cellText) continue; const trimmedText = cellText.trim(); // 文件名通常在第一列 if (j === 0 && trimmedText) { attachmentInfo.fileName = trimmedText; } // 日期检查 if (/^\d{4}-\d{2}-\d{2}/.test(trimmedText) || /^\d{4}\/\d{2}\/\d{2}/.test(trimmedText)) { if (!attachmentInfo.uploadDate) { attachmentInfo.uploadDate = trimmedText; } } // 上传者通常是文本用户名 if (j > 0 && trimmedText && !attachmentInfo.uploader && !attachmentInfo.uploadDate && !/^\d{4}/.test(trimmedText)) { attachmentInfo.uploader = trimmedText; } } if (attachmentInfo.fileName) { result.push(attachmentInfo); } } } else if (await attachmentList.count() > 0) { // 如果是列表形式 const listItems = attachmentList.locator('[class*="item"], [class*="row"], li, div'); const itemCount = await listItems.count(); for (let i = 0; i < itemCount; i++) { const item = listItems.nth(i); const itemText = await item.textContent(); if (itemText && itemText.trim()) { result.push({ fileName: itemText.trim() }); } } } return result; } /** * 关闭订单详情对话框 */ async closeDetailDialog(): Promise { // 尝试多种关闭方式 // 方式1: 点击右上角 X 按钮 const closeButton = this.page.locator('[role="dialog"]').getByRole('button', { name: '关闭' }).first(); const closeButtonCount = await closeButton.count(); if (closeButtonCount > 0) { await closeButton.click(); } else { // 方式2: 点击取消按钮 const cancelButton = this.page.locator('[role="dialog"]').getByRole('button', { name: '取消' }).first(); const cancelButtonCount = await cancelButton.count(); if (cancelButtonCount > 0) { await cancelButton.click(); } else { // 方式3: 按 Escape 键 await this.page.keyboard.press('Escape'); } } // 等待对话框关闭 await this.waitForDialogClosed(); } // ===== 人员关联管理 ===== /** * 打开人员管理对话框 * * **使用场景:** * - **从订单列表页打开**: 传入 `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); } else if (personData.disabledPersonId) { // 如果只提供了 ID,尝试在对话框中选择第一个残疾人 const firstCheckbox = this.page.locator('[role="dialog"]').locator('table tbody tr').first().locator('input[type="checkbox"]').first(); try { await firstCheckbox.waitFor({ state: 'visible', timeout: 3000 }); await firstCheckbox.check(); } catch { console.debug('没有可用的残疾人数据'); } } // 填写入职日期 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; } // ===== 订单状态流转操作 ===== /** * 打开激活订单确认对话框 * @param orderName 订单名称 */ async openActivateDialog(orderName: string): Promise { // 找到订单行并点击"打开菜单"按钮(与编辑/删除操作相同的模式) const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); await menuButton.click(); // 等待菜单出现并点击"激活"选项 const activateOption = this.page.getByRole('menuitem', { name: /激活|激活订单/ }); await activateOption.waitFor({ state: 'visible', timeout: 3000 }); await activateOption.click(); // 等待确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 }); } /** * 确认激活订单 */ async confirmActivate(): Promise { // 尝试多种可能的按钮名称 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); } /** * 激活订单(完整流程) * @param orderName 订单名称 * @returns 是否成功激活 */ async activateOrder(orderName: string): Promise { await this.openActivateDialog(orderName); await this.confirmActivate(); // 等待并检查 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; } /** * 打开关闭订单确认对话框 * @param orderName 订单名称 */ async openCloseDialog(orderName: string): Promise { // 找到订单行并点击"打开菜单"按钮 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); await menuButton.click(); // 等待菜单出现并点击"关闭"选项 const closeOption = this.page.getByRole('menuitem', { name: /关闭|关闭订单|完成/ }); await closeOption.waitFor({ state: 'visible', timeout: 3000 }); await closeOption.click(); // 等待确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 }); } /** * 确认关闭订单 */ async confirmClose(): Promise { // 尝试多种可能的按钮名称 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); } /** * 关闭订单(完整流程) * @param orderName 订单名称 * @returns 是否成功关闭 */ async closeOrder(orderName: string): Promise { await this.openCloseDialog(orderName); await this.confirmClose(); // 等待并检查 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; } /** * 获取订单的当前状态(从列表页面) * @param orderName 订单名称 * @returns 订单状态值或 null */ async getOrderStatus(orderName: string): Promise { const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); // 等待行可见 await orderRow.waitFor({ state: 'visible', timeout: 3000 }).catch(() => { console.debug(`订单 "${orderName}" 行不可见`); }); const rowCount = await orderRow.count(); if (rowCount === 0) { console.debug(`订单 "${orderName}" 不存在`); return null; } // 尝试多种策略定位状态列 // 策略1: 查找包含状态文本的单元格(但排除订单名称列) const allCells = orderRow.locator('td'); const cellCount = await allCells.count(); for (let i = 1; i < cellCount; i++) { // 跳过第一列(通常是订单名称) const cell = allCells.nth(i); const cellText = await cell.textContent(); if (cellText) { // 检查是否包含完整的状态标签(避免部分匹配) for (const [statusValue, statusLabel] of Object.entries(ORDER_STATUS_LABELS)) { // 使用更严格的匹配:必须是状态标签本身或包含完整标签 const trimmedText = cellText.trim(); if (trimmedText === statusLabel || trimmedText.includes(`${statusLabel}`)) { // 验证不是订单名称列(额外检查) const firstCellText = await allCells.nth(0).textContent(); if (firstCellText && !firstCellText.includes(orderName.substring(0, 3))) { // 第一列不包含订单名称开头,说明列结构可能不同 return statusValue as OrderStatus; } // 跳过第一列后找到的状态标签才返回 return statusValue as OrderStatus; } } } } // 策略2: 如果上述方法失败,尝试查找状态徽章/标签元素 // 查找具有状态样式特征的元素 const statusBadge = orderRow.locator('[class*="status"], [class*="badge"], span').filter({ hasText: Object.values(ORDER_STATUS_LABELS) }); if (await statusBadge.count() > 0) { const badgeText = await statusBadge.first().textContent(); if (badgeText) { for (const [statusValue, statusLabel] of Object.entries(ORDER_STATUS_LABELS)) { if (badgeText.includes(statusLabel)) { return statusValue as OrderStatus; } } } } console.debug(`无法从订单 "${orderName}" 中解析状态`); return null; } /** * 验证订单状态 * @param orderName 订单名称 * @param expectedStatus 期望的状态 */ async expectOrderStatus(orderName: string, expectedStatus: OrderStatus): Promise { const actualStatus = await this.getOrderStatus(orderName); if (actualStatus === null) { throw new Error(`订单 "${orderName}" 未找到或状态列无法识别`); } if (actualStatus !== expectedStatus) { throw new Error( `订单 "${orderName}" 状态不匹配: 期望 "${ORDER_STATUS_LABELS[expectedStatus]}", 实际 "${ORDER_STATUS_LABELS[actualStatus]}"` ); } } /** * 检查激活按钮是否可用 * * **注意**: 此方法会打开和关闭菜单,属于有副作用的操作 * * @param orderName 订单名称 * @returns 按钮是否可用 */ async checkActivateButtonEnabled(orderName: string): Promise { // 找到订单行并打开菜单 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); // 检查订单是否存在 const orderCount = await orderRow.count(); if (orderCount === 0) { console.debug(`订单 "${orderName}" 不存在`); return false; } const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); try { await menuButton.click(); } catch (error) { console.debug(`无法打开订单 "${orderName}" 的菜单:`, error); return false; } // 检查激活菜单项是否可点击 const activateOption = this.page.getByRole('menuitem', { name: /激活|激活订单/ }); const isVisible = await activateOption.isVisible().catch(() => false); let isEnabled = false; if (isVisible) { // 检查是否有禁用属性或样式 const isDisabled = await activateOption.isDisabled().catch(() => false); isEnabled = !isDisabled; } // 关闭菜单以便后续操作 await this.page.keyboard.press('Escape'); await this.page.waitForTimeout(300); return isEnabled; } /** * 检查关闭按钮是否可用 * * **注意**: 此方法会打开和关闭菜单,属于有副作用的操作 * * @param orderName 订单名称 * @returns 按钮是否可用 */ async checkCloseButtonEnabled(orderName: string): Promise { // 找到订单行并打开菜单 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }); // 检查订单是否存在 const orderCount = await orderRow.count(); if (orderCount === 0) { console.debug(`订单 "${orderName}" 不存在`); return false; } const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); try { await menuButton.click(); } catch (error) { console.debug(`无法打开订单 "${orderName}" 的菜单:`, error); return false; } // 检查关闭菜单项是否可点击 const closeOption = this.page.getByRole('menuitem', { name: /关闭|关闭订单|完成/ }); const isVisible = await closeOption.isVisible().catch(() => false); let isEnabled = false; if (isVisible) { // 检查是否有禁用属性或样式 const isDisabled = await closeOption.isDisabled().catch(() => false); isEnabled = !isDisabled; } // 关闭菜单以便后续操作 await this.page.keyboard.press('Escape'); await this.page.waitForTimeout(300); return isEnabled; } }