| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108 |
- 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<OrderStatus, string> = {
- 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<WorkStatus, string> = {
- 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<string, string>;
- /** 响应体 */
- 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<FormSubmitResult> {
- // 收集网络响应
- 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<boolean> {
- 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<string, string | undefined> = {};
- // 订单名称 - 查找"订单名称"标签后的值
- 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);
- } 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<FormSubmitResult> {
- 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<FormSubmitResult> {
- 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<boolean> {
- 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<void> {
- // 找到订单行并点击"打开菜单"按钮(与编辑/删除操作相同的模式)
- 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<void> {
- // 尝试多种可能的按钮名称
- 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<boolean> {
- 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<void> {
- // 找到订单行并点击"打开菜单"按钮
- 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<void> {
- // 尝试多种可能的按钮名称
- 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<boolean> {
- 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<OrderStatus | null> {
- 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<void> {
- 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<boolean> {
- // 找到订单行并打开菜单
- 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<boolean> {
- // 找到订单行并打开菜单
- 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;
- }
- }
|