| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581 |
- 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<OrderStatus, string> = {
- 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<WorkStatus, string> = {
- 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<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: 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 });
- 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 });
- 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.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('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() {
- 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: TIMEOUTS.DIALOG })
- .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 });
- // 先点击操作菜单触发按钮("打开菜单" 或 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<string, string | undefined> = {};
- // 使用 data-testid 直接定位元素(更可靠)
- // DOM 结构: <div className="flex items-center justify-between">
- // <span className="text-sm font-medium">标签:</span>
- // <span data-testid="order-detail-xxx">值</span>
- // </div>
-
- // 订单名称 - 使用 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<Array<{
- name?: string;
- workStatus?: string;
- hireDate?: string;
- salary?: string;
- }>> {
- 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<Array<{
- fileName?: string;
- uploadDate?: string;
- uploader?: string;
- }>> {
- 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<void> {
- // 尝试多种关闭方式
- // 方式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<WorkStatus, string> = {
- 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. 打开 FileSelector 对话框(第三个对话框)
- * 3. 在 FileSelector 对话框中使用 uploadFileToField 上传文件
- * 4. 点击上传后的文件进行选择
- * 5. 点击"确认选择"按钮
- *
- * @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;
- }
- // 点击对应的上传文件按钮,这会打开 FileSelector 对话框
- const targetButton = uploadButton.nth(buttonIndex);
- await targetButton.click();
- // FileSelector 对话框的 data-testid 是 "file-selector-dialog"
- const fileSelectorDialog = this.page.getByTestId('file-selector-dialog');
- // 等待 FileSelector 对话框打开(第三个对话框)
- // 使用 expect().toBeVisible() 主动等待,而不是被动超时
- 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 (_error) {
- console.debug('文件上传失败:', error);
- // 即使上传失败,也尝试关闭对话框
- 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('未找到"确认选择"按钮');
- }
- // 等待对话框关闭
- await this.page.waitForTimeout(TIMEOUTS.MEDIUM);
- 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<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(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<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: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
- await activateOption.click();
- // 等待确认对话框出现
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG });
- }
- /**
- * 确认激活订单
- */
- 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: 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<boolean> {
- 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<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: TIMEOUTS.ELEMENT_VISIBLE_SHORT });
- await closeOption.click();
- // 等待确认对话框出现
- await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: TIMEOUTS.DIALOG });
- }
- /**
- * 确认关闭订单
- */
- 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: 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<boolean> {
- 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<OrderStatus | null> {
- const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
- // 等待行可见
- 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<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(TIMEOUTS.SHORT);
- 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(TIMEOUTS.SHORT);
- return isEnabled;
- }
- }
|