import { TIMEOUTS } from '../../utils/timeouts'; 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_WORKING: 'not_working', PRE_WORKING: 'pre_working', WORKING: 'working', RESIGNED: 'resigned', } as const; /** * 工作状态类型 */ export type WorkStatus = typeof WORK_STATUS[keyof typeof WORK_STATUS]; /** * 工作状态显示名称映射 */ export const WORK_STATUS_LABELS: Record = { not_working: '未入职', pre_working: '已入职', working: '工作中', 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: TIMEOUTS.PAGE_LOAD }); // 等待表格数据加载 await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: TIMEOUTS.PAGE_LOAD_LONG }); await this.expectToBeVisible(); } /** * 验证页面关键元素可见 */ async expectToBeVisible() { await this.pageTitle.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD }); await this.addOrderButton.waitFor({ state: 'visible', timeout: TIMEOUTS.TABLE_LOAD }); } // ===== 搜索和筛选功能 ===== /** * 按订单名称搜索 * @param name 订单名称 */ async searchByName(name: string) { await this.searchInput.fill(name); await this.searchButton.click(); await this.page.waitForLoadState('networkidle'); await this.page.waitForTimeout(TIMEOUTS.LONG); } /** * 打开高级筛选对话框 */ async openFilterDialog() { const filterButton = this.page.getByRole('button', { name: /筛选|高级筛选/ }); await filterButton.click(); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 设置筛选条件 * @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(TIMEOUTS.LONG); } /** * 清空筛选条件 */ async clearFilters() { const clearButton = this.page.getByRole('button', { name: /重置|清空/ }); await clearButton.click(); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } // ===== 订单 CRUD 操作 ===== /** * 打开创建订单对话框 */ async openCreateDialog() { await this.addOrderButton.click(); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开编辑订单对话框 * @param orderName 订单名称 */ async openEditDialog(orderName: string) { // 找到订单行并点击"打开菜单"按钮 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }).first(); 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: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await editOption.click(); // 等待编辑对话框出现 await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 打开删除确认对话框 * @param orderName 订单名称 */ async openDeleteDialog(orderName: string) { // 找到订单行并点击"打开菜单"按钮(与编辑操作相同的模式) const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }).first(); const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); await menuButton.click(); // 等待菜单出现并点击"删除"选项 const deleteOption = this.page.getByRole('menuitem', { name: '删除' }); await deleteOption.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await deleteOption.click(); // 等待删除确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 填写订单表单 * @param data 订单数据 */ async fillOrderForm(data: OrderData) { // 等待表单出现 await this.page.waitForSelector('form', { state: 'visible', timeout: TIMEOUTS.DIALOG }); // 填写订单名称 if (data.name) { await this.page.getByLabel(/订单名称|名称/).fill(data.name); } // 选择平台(必须在公司之前选择,因为公司列表依赖平台) 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.expectedStartDate) { const dateInput = this.page.getByLabel(/预计开始日期|开始日期/); await dateInput.fill(data.expectedStartDate); } // 选择订单状态(如果是编辑模式) 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); } // 创建订单时需要至少选择一名残疾人 // 如果表单中有"选择残疾人"按钮,点击它并选择第一个可用的残疾人 const selectPersonButton = this.page.getByRole('button', { name: '选择残疾人' }); const hasSelectPersonButton = await selectPersonButton.count(); if (hasSelectPersonButton > 0) { console.debug('[创建订单] 检测到需要选择残疾人,点击"选择残疾人"按钮'); await selectPersonButton.click(); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); // 等待残疾人选择器对话框打开 // 选择第一个可用的残疾人(通常是测试数据) // 尝试多种方式定位残疾人列表 const firstCheckbox = this.page.locator('input[type="checkbox"]').first(); const checkboxCount = await firstCheckbox.count(); if (checkboxCount > 0) { // 使用第一个复选框 await firstCheckbox.click(); console.debug('[创建订单] 已选择第一个残疾人'); // 查找确认按钮并点击(可能是"确定"、"确认"等) const confirmButton = this.page.getByRole('button', { name: /^(确定|确认|选择)$/ }); const confirmCount = await confirmButton.count(); if (confirmCount > 0) { await confirmButton.first().click(); console.debug('[创建订单] 已确认选择残疾人'); } else { // 如果没有确认按钮,尝试按 Enter 键 await this.page.keyboard.press('Enter'); console.debug('[创建订单] 按 Enter 键确认选择'); } } else { console.debug('[创建订单] 未找到残疾人复选框,尝试其他方式'); // 尝试查找残疾人列表项并点击第一个 const firstPersonItem = this.page.locator('[role="option"], .option-item, .person-item').first(); const itemCount = await firstPersonItem.count(); if (itemCount > 0) { await firstPersonItem.click(); console.debug('[创建订单] 已点击第一个残疾人选项'); } else { // 如果还是没有,尝试关闭对话框并继续(有些实现可能有默认选择) console.debug('[创建订单] 未找到残疾人选项,尝试关闭对话框并继续'); await this.page.keyboard.press('Escape'); } } await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } else { console.debug('[创建订单] 未检测到"选择残疾人"按钮,可能已有人选或不在创建模式'); } } /** * 提交表单 * @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: TIMEOUTS.DIALOG }); } catch { // domcontentloaded 超时不是致命错误,继续检查 Toast 消息 console.debug('domcontentloaded 超时,继续检查 Toast 消息'); } } finally { // 确保监听器总是被移除,防止内存泄漏 this.page.off('response', responseHandler); } // 等待 Toast 消息显示 await this.page.waitForTimeout(TIMEOUTS.VERY_LONG); // 检查 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(TIMEOUTS.LONG); // 检查是否还有对话框可见 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: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); console.debug('对话框已关闭'); } catch { // 超时不是致命错误,对话框可能已经以其他方式关闭 console.debug('对话框关闭等待超时,继续执行'); } await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } /** * 确认删除操作 */ async confirmDelete() { // 尝试多种可能的按钮名称 const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', { name: /^(确认删除|删除|确定|确认)$/ }); await confirmButton.click(); // 等待确认对话框关闭和网络请求完成 await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: TIMEOUTS.DIALOG }) .catch(() => console.debug('删除确认对话框关闭超时')); await this.page.waitForLoadState('networkidle', { timeout: TIMEOUTS.TABLE_LOAD }); await this.page.waitForTimeout(TIMEOUTS.LONG); } /** * 取消删除操作 */ async cancelDelete() { // 先定位到 alertdialog,然后在其中查找取消按钮 const dialog = this.page.locator('[role="alertdialog"]'); const cancelButton = dialog.getByRole('button', { name: '取消' }); await cancelButton.click(); await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: TIMEOUTS.DIALOG }) .catch(() => console.debug('删除确认对话框关闭超时(取消操作)')); } /** * 验证订单是否存在 * @param orderName 订单名称 * @returns 订单是否存在 */ async orderExists(orderName: string): Promise { const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }).first(); return (await orderRow.count()) > 0; } // ===== 订单详情 ===== /** * 打开订单详情对话框 * @param orderName 订单名称 */ async openDetailDialog(orderName: string) { // 找到订单行 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }).first(); // 先点击操作菜单触发按钮("打开菜单" 或 MoreHorizontal 图标) const menuTrigger = orderRow.getByRole('button', { name: /打开菜单/ }); await menuTrigger.click(); // 等待菜单显示 await this.page.waitForTimeout(TIMEOUTS.VERY_SHORT); // 点击"查看详情"菜单项 const detailButton = this.page.getByRole('menuitem', { name: /查看详情/ }); await detailButton.click(); await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 获取订单详情对话框中的基本信息 * @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 = {}; // 使用 data-testid 直接定位元素(更可靠) // DOM 结构:
// 标签: // //
// 订单名称 - 使用 data-testid const nameElement = dialog.locator('[data-testid="order-detail-name"]'); if (await nameElement.count() > 0) { result.name = (await nameElement.textContent())?.trim(); } // 订单状态 const statusElement = dialog.locator('[data-testid="order-detail-status"]'); if (await statusElement.count() > 0) { result.status = (await statusElement.textContent())?.trim(); } // 工作状态 - 查找包含"工作状态"标签的行 const workStatusRow = dialog.locator('div').filter({ hasText: /工作状态:/ }).first(); if (await workStatusRow.count() > 0) { const workStatusElement = workStatusRow.locator('span').nth(1); result.workStatus = (await workStatusElement.textContent())?.trim(); } // 预计开始日期 - 使用 data-testid const expectedStartDateElement = dialog.locator('[data-testid="order-detail-expected-start"]'); if (await expectedStartDateElement.count() > 0) { result.expectedStartDate = (await expectedStartDateElement.textContent())?.trim(); } // 平台 - 使用 data-testid const platformElement = dialog.locator('[data-testid="order-detail-platform"]'); if (await platformElement.count() > 0) { result.platform = (await platformElement.textContent())?.trim(); } // 公司 - 使用 data-testid const companyElement = dialog.locator('[data-testid="order-detail-company"]'); if (await companyElement.count() > 0) { result.company = (await companyElement.textContent())?.trim(); } // 渠道 - 使用 data-testid const channelElement = dialog.locator('[data-testid="order-detail-channel"]'); if (await channelElement.count() > 0) { result.channel = (await channelElement.textContent())?.trim(); } return result; } /** * 从订单详情对话框中获取关联人员列表 * @returns 人员信息列表 */ async getPersonListFromDetail(): Promise> { const dialog = this.page.locator('[role="dialog"]'); const result: Array<{ name?: string; workStatus?: string; hireDate?: string; salary?: string }> = []; // 查找所有表格,对话框中可能有两个表格: // 1. "待添加人员列表" - 临时表格,包含未确认的人员 // 2. "绑定人员列表" - 实际已绑定到订单的人员 // 我们需要第二个"绑定人员列表"表格 const allTables = dialog.locator('table'); const tableCount = await allTables.count(); // 查找"绑定人员列表"表格(通常是包含"工作状态"列的表格) let personTable; for (let i = 0; i < tableCount; i++) { const table = allTables.nth(i); const tableText = await table.textContent(); // 绑定人员列表表格包含"工作状态"列,而待添加人员列表没有 if (tableText && tableText.includes('工作状态')) { personTable = table; break; } } const personList = dialog.locator('[class*="person"], [class*="employee"], [data-testid*="person"]'); // 优先使用表格形式 if (personTable) { 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 } = {}; // 根据列数量和数据类型提取信息 // 表格列:ID 姓名 性别 残疾类型 联系电话 入职日期 离职日期 工作状态 薪资 for (let j = 0; j < cellCount; j++) { const cellText = await cells.nth(j).textContent(); if (!cellText) continue; const trimmedText = cellText.trim(); // 尝试识别列内容 // ID 在第一列(j === 0),姓名在第二列(j === 1) if (j === 1 && trimmedText) { personInfo.name = trimmedText; } // 工作状态检查 for (const [_statusValue, statusLabel] of Object.entries(WORK_STATUS_LABELS)) { if (trimmedText.includes(statusLabel)) { personInfo.workStatus = statusLabel; break; } } // 日期检查(符合日期格式) if (/^\d{4}-\d{2}-\d{2}$/.test(trimmedText) || /^\d{4}\/\d{2}\/\d{2}$/.test(trimmedText)) { if (!personInfo.hireDate) { personInfo.hireDate = trimmedText; } } // 薪资检查(在最后一列,包含数字且可能是薪资) // 薪资通常是较大的数字,不应该是11位电话号码 if (j === cellCount - 1 && /^\d+(\.\d+)?$/.test(trimmedText.replace(/,/g, ''))) { const numValue = trimmedText.replace(/,/g, ''); // 排除11位电话号码(如13800019729) if (numValue.length < 11) { personInfo.salary = 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) { await this.openDetailDialog(orderName); } // 人员管理功能已在详情对话框中,无需额外操作 } /** * 添加人员到订单 * @param personData 人员数据 */ async addPersonToOrder(personData: OrderPersonData) { // 点击添加人员按钮 const addButton = this.page.getByRole('button', { name: /添加人员|新增人员/ }); await addButton.click(); await this.page.waitForTimeout(TIMEOUTS.SHORT); // 选择残疾人(支持通过名称选择) 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: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); 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(TIMEOUTS.LONG); } /** * 修改人员工作状态 * @param personName 人员姓名 * @param newStatus 新的工作状态 */ async updatePersonWorkStatus(personName: string, newStatus: WorkStatus) { const dialog = this.page.locator('[role="dialog"]'); // 等待对话框完全加载 await dialog.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); // 从 error-context.md 可知: // 1. 对话框中有"绑定人员列表"表格 // 2. 表格列:ID 姓名 性别 残疾类型 联系电话 入职日期 离职日期 工作状态 薪资 // 3. 工作状态列直接是 combobox,不需要点击编辑按钮 // 查找所有表格 const allTables = dialog.locator('table'); const allTableCount = await allTables.count(); console.debug(`对话框中总共有 ${allTableCount} 个表格`); let personTable = allTables.first(); // 找到包含"绑定人员"或"工作状态"列的表格(第二个表格是绑定人员列表) for (let i = 0; i < allTableCount; i++) { const table = allTables.nth(i); const tableText = await table.textContent(); if (tableText && (tableText.includes('绑定人员') || tableText.includes('工作状态'))) { personTable = table; console.debug(`找到人员表格(索引 ${i})`); break; } } // 在表格中查找包含指定人员名称的行 const targetRow = personTable.locator('tbody tr').filter({ hasText: personName }).first(); const rowCount = await targetRow.count(); console.debug(`找到 ${rowCount} 个匹配的人员行`); if (rowCount === 0) { throw new Error(`未找到人员 ${personName}`); } // 等待行可见 await targetRow.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); // 从 error-context.md 可知,工作状态在单元格中是一个 combobox // 表格列:ID 姓名 性别 残疾类型 联系电话 入职日期 离职日期 工作状态 薪资 // 工作状态是倒数第二列(薪资是最后一列) const cells = targetRow.locator('td'); const cellCount = await cells.count(); console.debug(`人员行有 ${cellCount} 个单元格`); // 工作状态在倒数第二列 const workStatusCell = cells.nth(cellCount - 2); const workStatusCombobox = workStatusCell.getByRole('combobox'); const comboboxCount = await workStatusCombobox.count(); console.debug(`工作状态 combobox 数量: ${comboboxCount}`); if (comboboxCount === 0) { throw new Error(`未找到人员 ${personName} 的工作状态选择器`); } await workStatusCombobox.click({ timeout: TIMEOUTS.DIALOG }); console.debug('工作状态 combobox 已点击'); // 等待下拉选项显示 await this.page.waitForTimeout(TIMEOUTS.MEDIUM); // 使用中文标签选择选项 // 注意:UI 中的工作状态选项与 WORK_STATUS_LABELS 不同 // UI 选项:未入职、已入职、工作中、已离职 // WORK_STATUS_LABELS:未就业、待就业、已就业、已离职 const statusMapping: Record = { not_working: '未入职', pre_working: '已入职', working: '工作中', resigned: '已离职', }; const newWorkStatusLabel = statusMapping[newStatus]; console.debug(`尝试选择状态: ${newWorkStatusLabel}`); const optionLocator = this.page.getByRole('option', { name: newWorkStatusLabel }); const optionCount = await optionLocator.count(); console.debug(`找到 ${optionCount} 个选项`); if (optionCount === 0) { throw new Error(`未找到工作状态选项: ${newWorkStatusLabel}`); } await optionLocator.first().click({ timeout: TIMEOUTS.DIALOG }); console.debug(`工作状态已更新为: ${newWorkStatusLabel}`); // 使用较短的超时时间等待网络空闲 await this.page.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.DIALOG }) .catch(() => console.debug('domcontentloaded 等待超时,继续')); await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } // ===== 附件管理 ===== /** * 打开资源上传对话框 * "资源上传"按钮在订单详情对话框中 */ async openAddAttachmentDialog() { // 使用"资源上传"按钮 const attachmentButton = this.page.getByRole('button', { name: /资源上传/ }); await attachmentButton.click(); // 等待第二个对话框(资源上传对话框)打开 await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } /** * 上传附件 * * 实际的 UI 流程: * 1. 在资源上传对话框中,点击人员行中对应文件类型的"上传文件"按钮 * 2. 打开上传弹窗(第三个对话框)- 选择文件类型 * 3. 在上传弹窗中,点击 FileSelector 的触发按钮 * 4. 打开 FileSelector 对话框(第四个对话框) * 5. 在 FileSelector 对话框中使用 uploadFileToField 上传文件 * 6. 点击上传后的文件进行选择 * 7. 点击"确认选择"按钮 * 8. 在上传弹窗中点击"提交"按钮 * * @param personIdentifier 人员标识(可以是 ID 或姓名,方法会自动匹配) * @param fileName 文件名(相对于 web/tests/fixtures 目录) * @param mimeType 文件类型(默认为 image/jpeg,未使用,保留用于未来扩展) * @param fileType 文件类型(税务文件、薪资单、工作成果、合同签署、残疾证明、其他),默认为"其他" */ async uploadAttachment( personIdentifier: string, fileName: string, _mimeType: string = 'image/jpeg', fileType: string = '其他' ) { // 动态导入 uploadFileToField 工具 const { uploadFileToField } = await import('@d8d/e2e-test-utils'); // 找到资源上传对话框(第二个对话框) const dialogs = this.page.locator('[role="dialog"]'); const uploadDialog = dialogs.nth(1); // 在对话框中找到对应残疾人的行 // 使用 ID 或姓名匹配 const personRow = uploadDialog.locator('tr').filter({ hasText: personIdentifier }); const rowCount = await personRow.count(); if (rowCount === 0) { console.debug(`未找到人员 ${personIdentifier} 的行`); // 尝试打印所有行内容用于调试 const allRows = uploadDialog.locator('tbody tr'); const allRowCount = await allRows.count(); console.debug(`资源上传对话框共有 ${allRowCount} 行`); for (let i = 0; i < Math.min(allRowCount, 3); i++) { const rowText = await allRows.nth(i).textContent(); console.debug(`行 ${i} 内容:`, rowText); } return; } console.debug(`找到人员 ${personIdentifier} 的行`); // 在该人员行中找到对应文件类型的"上传文件"按钮 // 文件类型列顺序:税务文件、薪资单、工作成果、合同签署、残疾证明、其他 const uploadButton = personRow.getByRole('button', { name: '上传文件' }); const buttonCount = await uploadButton.count(); if (buttonCount === 0) { console.debug(`未找到"上传文件"按钮`); return; } console.debug(`找到 ${buttonCount} 个"上传文件"按钮`); // 根据文件类型选择对应的上传按钮 let buttonIndex = 5; // 默认为"其他"(最后一个) switch (fileType) { case '税务文件': buttonIndex = 0; break; case '薪资单': buttonIndex = 1; break; case '工作成果': buttonIndex = 2; break; case '合同签署': buttonIndex = 3; break; case '残疾证明': buttonIndex = 4; break; case '其他': default: buttonIndex = 5; break; } // 点击对应的上传文件按钮,这会打开上传弹窗(第三个对话框) const targetButton = uploadButton.nth(buttonIndex); // 调试信息:检查按钮是否可见 const isVisible = await targetButton.isVisible().catch(() => false); console.debug(`目标上传文件按钮可见性: ${isVisible}`); if (!isVisible) { console.debug(`上传文件按钮不可见,尝试滚动到视图`); await personRow.scrollIntoViewIfNeeded(); await this.page.waitForTimeout(TIMEOUTS.SHORT); } await targetButton.click(); console.debug(`已点击第 ${buttonIndex} 个上传文件按钮`); // 等待上传弹窗打开(第三个对话框) await this.page.waitForTimeout(TIMEOUTS.MEDIUM); // 在上传弹窗中点击 FileSelector 的触发按钮 // FileSelector 组件的触发按钮文本是"选择或上传文件" const fileSelectorTrigger = this.page.getByRole('button', { name: /选择或上传文件/ }).or( this.page.getByText('选择或上传文件') ); const triggerCount = await fileSelectorTrigger.count(); console.debug(`找到 ${triggerCount} 个 FileSelector 触发按钮`); if (triggerCount === 0) { console.debug('未找到 FileSelector 触发按钮'); return; } // 点击最新的 FileSelector 触发按钮(如果有多于一个的话) await fileSelectorTrigger.nth(triggerCount - 1).click(); console.debug('已点击 FileSelector 触发按钮'); // FileSelector 对话框的 data-testid 是 "file-selector-dialog" const fileSelectorDialog = this.page.getByTestId('file-selector-dialog'); // 等待 FileSelector 对话框打开(第四个对话框) try { await fileSelectorDialog.waitFor({ state: 'visible', timeout: TIMEOUTS.DIALOG }); console.debug('FileSelector 对话框已打开'); } catch (_error) { console.debug('FileSelector 对话框未打开(超时)'); // 打印调试信息 const allDialogs = await this.page.locator('[role="dialog"]').count(); console.debug(`当前页面对话框数量: ${allDialogs}`); return; } // 使用 uploadFileToField 上传文件 // MinioUploader 使用 data-testid="minio-uploader-input"(因为 testId 未被传递) try { await uploadFileToField( this.page, '[data-testid="minio-uploader-input"]', fileName, { fixturesDir: 'tests/fixtures', timeout: TIMEOUTS.DIALOG } ); console.debug(`文件 ${fileName} 上传操作已完成`); } catch (uploadError) { console.debug('文件上传失败:', uploadError); // 即使上传失败,也尝试关闭对话框 await fileSelectorDialog.getByRole('button', { name: '取消' }).click().catch(() => {}); return; } // 等待上传处理完成,等待文件出现在对话框中 await this.page.waitForTimeout(TIMEOUTS.LONG); // 点击上传后的文件进行选择(查找第一个可点击的文件) // 上传成功后,文件会被添加到文件列表中并显示为选中状态(border-primary) const uploadedFile = fileSelectorDialog.locator('.border-primary').or( fileSelectorDialog.locator('img').first() ); const fileExists = await uploadedFile.count() > 0; if (fileExists) { await uploadedFile.first().click(); console.debug('已点击上传后的文件'); } else { // 如果没找到选中的文件,尝试查看对话框内容 const allImages = fileSelectorDialog.locator('img'); const imgCount = await allImages.count(); console.debug(`对话框中图片数量: ${imgCount}`); // 尝试点击任何图片 if (imgCount > 0) { await allImages.first().click(); console.debug('已点击第一张图片'); } } await this.page.waitForTimeout(TIMEOUTS.SHORT); // 点击"确认选择"按钮 const confirmButton = fileSelectorDialog.getByRole('button', { name: '确认选择' }); const confirmButtonCount = await confirmButton.count(); if (confirmButtonCount > 0) { await confirmButton.click(); console.debug('已点击确认选择按钮'); } else { console.debug('未找到"确认选择"按钮'); } // 等待 FileSelector 对话框关闭(回到上传弹窗) await this.page.waitForTimeout(TIMEOUTS.MEDIUM); // 在上传弹窗中点击"提交"按钮 const submitButton = this.page.getByRole('button', { name: /^提交$/ }); const submitButtonCount = await submitButton.count(); if (submitButtonCount > 0) { await submitButton.click(); console.debug('已点击提交按钮'); // 等待提交完成(上传弹窗关闭) await this.page.waitForTimeout(TIMEOUTS.LONG); } else { console.debug('未找到提交按钮'); } console.debug(`附件上传流程完成: ${fileName}`); } /** * 关闭资源上传对话框 */ async closeUploadDialog() { // 资源上传对话框有"关闭"按钮 // 需要精确定位到第二个对话框(资源上传对话框)的关闭按钮 const dialogs = this.page.locator('[role="dialog"]'); const dialogCount = await dialogs.count(); if (dialogCount >= 2) { // 资源上传对话框通常是第二个对话框 const uploadDialog = dialogs.nth(1); const closeButton = uploadDialog.getByRole('button', { name: '关闭' }); const buttonCount = await closeButton.count(); if (buttonCount > 0) { await closeButton.first().click(); console.debug('已关闭资源上传对话框'); } else { console.debug('未找到资源上传对话框的关闭按钮,尝试按 Escape'); await this.page.keyboard.press('Escape'); } } else { // 如果只有一个对话框,尝试点击通用的关闭按钮 const closeButton = this.page.getByRole('button', { name: '关闭' }); const buttonCount = await closeButton.count(); if (buttonCount > 0) { await closeButton.first().click(); console.debug('已关闭对话框(使用通用关闭按钮)'); } } await this.page.waitForTimeout(TIMEOUTS.MEDIUM); } // ===== 高级操作 ===== /** * 创建订单(完整流程) * @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(TIMEOUTS.LONG); 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 }).first(); const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); await menuButton.click(); // 等待菜单出现并点击"激活"选项 const activateOption = this.page.getByRole('menuitem', { name: /激活|激活订单/ }); await activateOption.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await activateOption.click(); // 等待确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 确认激活订单 */ 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: TIMEOUTS.DIALOG }) .catch(() => console.debug('激活确认对话框关闭超时')); // networkidle 可能因为持续的网络活动而失败,使用更宽松的超时 await this.page.waitForLoadState('networkidle', { timeout: TIMEOUTS.TABLE_LOAD }) .catch(() => console.debug('networkidle 超时,继续执行')); await this.page.waitForTimeout(TIMEOUTS.LONG); } /** * 激活订单(完整流程) * @param orderName 订单名称 * @returns 是否成功激活 */ async activateOrder(orderName: string): Promise { await this.openActivateDialog(orderName); await this.confirmActivate(); // 等待并检查 Toast 消息 await this.page.waitForTimeout(TIMEOUTS.LONG); 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 }).first(); const menuButton = orderRow.getByRole('button', { name: '打开菜单' }); await menuButton.click(); // 等待菜单出现并点击"关闭"选项 const closeOption = this.page.getByRole('menuitem', { name: /关闭|关闭订单|完成/ }); await closeOption.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }); await closeOption.click(); // 等待确认对话框出现 await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG }); } /** * 确认关闭订单 */ 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: TIMEOUTS.DIALOG }) .catch(() => console.debug('关闭确认对话框关闭超时')); await this.page.waitForLoadState('networkidle', { timeout: TIMEOUTS.TABLE_LOAD }); await this.page.waitForTimeout(TIMEOUTS.LONG); } /** * 关闭订单(完整流程) * @param orderName 订单名称 * @returns 是否成功关闭 */ async closeOrder(orderName: string): Promise { await this.openCloseDialog(orderName); await this.confirmClose(); // 等待并检查 Toast 消息 await this.page.waitForTimeout(TIMEOUTS.LONG); 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 }).first(); // 等待行可见 await orderRow.waitFor({ state: 'visible', timeout: TIMEOUTS.ELEMENT_VISIBLE_SHORT }).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 }).first(); // 检查订单是否存在 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(TIMEOUTS.SHORT); return isEnabled; } /** * 检查关闭按钮是否可用 * * **注意**: 此方法会打开和关闭菜单,属于有副作用的操作 * * @param orderName 订单名称 * @returns 按钮是否可用 */ async checkCloseButtonEnabled(orderName: string): Promise { // 找到订单行并打开菜单 const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName }).first(); // 检查订单是否存在 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(TIMEOUTS.SHORT); return isEnabled; } }