order-management.page.ts 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381
  1. import { Page, Locator } from '@playwright/test';
  2. import { selectRadixOption } from '@d8d/e2e-test-utils';
  3. /**
  4. * 订单状态常量
  5. */
  6. export const ORDER_STATUS = {
  7. DRAFT: 'draft',
  8. CONFIRMED: 'confirmed',
  9. IN_PROGRESS: 'in_progress',
  10. COMPLETED: 'completed',
  11. } as const;
  12. /**
  13. * 订单状态类型
  14. */
  15. export type OrderStatus = typeof ORDER_STATUS[keyof typeof ORDER_STATUS];
  16. /**
  17. * 订单状态显示名称映射
  18. */
  19. export const ORDER_STATUS_LABELS: Record<OrderStatus, string> = {
  20. draft: '草稿',
  21. confirmed: '已确认',
  22. in_progress: '进行中',
  23. completed: '已完成',
  24. } as const;
  25. /**
  26. * 工作状态常量
  27. */
  28. export const WORK_STATUS = {
  29. NOT_WORKING: 'not_working',
  30. PRE_WORKING: 'pre_working',
  31. WORKING: 'working',
  32. RESIGNED: 'resigned',
  33. } as const;
  34. /**
  35. * 工作状态类型
  36. */
  37. export type WorkStatus = typeof WORK_STATUS[keyof typeof WORK_STATUS];
  38. /**
  39. * 工作状态显示名称映射
  40. */
  41. export const WORK_STATUS_LABELS: Record<WorkStatus, string> = {
  42. not_working: '未入职',
  43. pre_working: '已入职',
  44. working: '工作中',
  45. resigned: '已离职',
  46. } as const;
  47. /**
  48. * 订单数据接口
  49. */
  50. export interface OrderData {
  51. /** 订单名称 */
  52. name: string;
  53. /** 预计开始日期 */
  54. expectedStartDate?: string;
  55. /** 平台ID */
  56. platformId?: number;
  57. /** 平台名称 */
  58. platformName?: string;
  59. /** 公司ID */
  60. companyId?: number;
  61. /** 公司名称 */
  62. companyName?: string;
  63. /** 渠道ID */
  64. channelId?: number;
  65. /** 渠道名称 */
  66. channelName?: string;
  67. /** 订单状态 */
  68. status?: OrderStatus;
  69. /** 工作状态 */
  70. workStatus?: WorkStatus;
  71. }
  72. /**
  73. * 订单人员数据接口
  74. */
  75. export interface OrderPersonData {
  76. /** 残疾人ID */
  77. disabledPersonId: number;
  78. /** 残疾人姓名 */
  79. disabledPersonName?: string;
  80. /** 入职日期 */
  81. hireDate?: string;
  82. /** 薪资 */
  83. salary?: number;
  84. /** 工作状态 */
  85. workStatus?: WorkStatus;
  86. /** 实际入职日期 */
  87. actualHireDate?: string;
  88. /** 离职日期 */
  89. resignDate?: string;
  90. }
  91. /**
  92. * 网络响应数据接口
  93. */
  94. export interface NetworkResponse {
  95. /** 请求URL */
  96. url: string;
  97. /** 请求方法 */
  98. method: string;
  99. /** 响应状态码 */
  100. status: number;
  101. /** 是否成功 */
  102. ok: boolean;
  103. /** 响应头 */
  104. responseHeaders: Record<string, string>;
  105. /** 响应体 */
  106. responseBody: unknown;
  107. }
  108. /**
  109. * 表单提交结果接口
  110. */
  111. export interface FormSubmitResult {
  112. /** 提交是否成功 */
  113. success: boolean;
  114. /** 是否有错误 */
  115. hasError: boolean;
  116. /** 是否有成功消息 */
  117. hasSuccess: boolean;
  118. /** 错误消息 */
  119. errorMessage?: string;
  120. /** 成功消息 */
  121. successMessage?: string;
  122. /** 网络响应列表 */
  123. responses?: NetworkResponse[];
  124. }
  125. /**
  126. * 订单管理 Page Object
  127. *
  128. * 用于订单管理功能的 E2E 测试
  129. * 页面路径: /admin/orders(待确认)
  130. *
  131. * @example
  132. * ```typescript
  133. * const orderPage = new OrderManagementPage(page);
  134. * await orderPage.goto();
  135. * await orderPage.createOrder({ name: '测试订单' });
  136. * ```
  137. */
  138. export class OrderManagementPage {
  139. readonly page: Page;
  140. // ===== 页面级选择器 =====
  141. /** 页面标题 */
  142. readonly pageTitle: Locator;
  143. /** 新增订单按钮 */
  144. readonly addOrderButton: Locator;
  145. /** 订单列表表格 */
  146. readonly orderTable: Locator;
  147. /** 搜索输入框 */
  148. readonly searchInput: Locator;
  149. /** 搜索按钮 */
  150. readonly searchButton: Locator;
  151. constructor(page: Page) {
  152. this.page = page;
  153. // 初始化页面级选择器
  154. // 使用更精确的选择器来定位页面标题(避免与侧边栏按钮冲突)
  155. this.pageTitle = page.locator('[data-slot="card-title"]').getByText('订单管理', { exact: true });
  156. // 使用 data-testid 定位创建订单按钮(按钮文本是"创建订单"不是"新增订单")
  157. this.addOrderButton = page.getByTestId('create-order-button');
  158. this.orderTable = page.locator('table');
  159. // 使用 data-testid 定位搜索输入框
  160. this.searchInput = page.getByTestId('search-order-name-input');
  161. // 使用 data-testid 定位搜索按钮
  162. this.searchButton = page.getByTestId('search-button');
  163. }
  164. // ===== 导航和基础验证 =====
  165. /**
  166. * 导航到订单管理页面
  167. */
  168. async goto() {
  169. await this.page.goto('/admin/orders');
  170. await this.page.waitForLoadState('domcontentloaded');
  171. // 等待页面标题出现
  172. await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
  173. // 等待表格数据加载
  174. await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: 20000 });
  175. await this.expectToBeVisible();
  176. }
  177. /**
  178. * 验证页面关键元素可见
  179. */
  180. async expectToBeVisible() {
  181. await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
  182. await this.addOrderButton.waitFor({ state: 'visible', timeout: 10000 });
  183. }
  184. // ===== 搜索和筛选功能 =====
  185. /**
  186. * 按订单名称搜索
  187. * @param name 订单名称
  188. */
  189. async searchByName(name: string) {
  190. await this.searchInput.fill(name);
  191. await this.searchButton.click();
  192. await this.page.waitForLoadState('networkidle');
  193. await this.page.waitForTimeout(1000);
  194. }
  195. /**
  196. * 打开高级筛选对话框
  197. */
  198. async openFilterDialog() {
  199. const filterButton = this.page.getByRole('button', { name: /筛选|高级筛选/ });
  200. await filterButton.click();
  201. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  202. }
  203. /**
  204. * 设置筛选条件
  205. * @param filters 筛选条件
  206. */
  207. async setFilters(filters: {
  208. status?: OrderStatus;
  209. workStatus?: WorkStatus;
  210. platformId?: number;
  211. platformName?: string;
  212. companyId?: number;
  213. companyName?: string;
  214. channelId?: number;
  215. channelName?: string;
  216. dateRange?: { start?: string; end?: string };
  217. }) {
  218. // 订单状态筛选
  219. if (filters.status) {
  220. const statusFilter = this.page.getByLabel(/订单状态/);
  221. await statusFilter.click();
  222. const statusLabel = ORDER_STATUS_LABELS[filters.status];
  223. await this.page.getByRole('option', { name: statusLabel }).click();
  224. }
  225. // 工作状态筛选
  226. if (filters.workStatus) {
  227. const workStatusFilter = this.page.getByLabel(/工作状态/);
  228. await workStatusFilter.click();
  229. const workStatusLabel = WORK_STATUS_LABELS[filters.workStatus];
  230. await this.page.getByRole('option', { name: workStatusLabel }).click();
  231. }
  232. // 平台筛选
  233. if (filters.platformName) {
  234. await selectRadixOption(this.page, '平台', filters.platformName);
  235. }
  236. // 公司筛选
  237. if (filters.companyName) {
  238. await selectRadixOption(this.page, '公司', filters.companyName);
  239. }
  240. // 渠道筛选
  241. if (filters.channelName) {
  242. await selectRadixOption(this.page, '渠道', filters.channelName);
  243. }
  244. // 日期范围筛选
  245. if (filters.dateRange) {
  246. if (filters.dateRange.start) {
  247. const startDateInput = this.page.getByLabel(/开始日期|起始日期/);
  248. await startDateInput.fill(filters.dateRange.start);
  249. }
  250. if (filters.dateRange.end) {
  251. const endDateInput = this.page.getByLabel(/结束日期|截止日期/);
  252. await endDateInput.fill(filters.dateRange.end);
  253. }
  254. }
  255. }
  256. /**
  257. * 应用筛选条件
  258. */
  259. async applyFilters() {
  260. const applyButton = this.page.getByRole('button', { name: /应用|确定|筛选/ });
  261. await applyButton.click();
  262. await this.page.waitForLoadState('networkidle');
  263. await this.page.waitForTimeout(1000);
  264. }
  265. /**
  266. * 清空筛选条件
  267. */
  268. async clearFilters() {
  269. const clearButton = this.page.getByRole('button', { name: /重置|清空/ });
  270. await clearButton.click();
  271. await this.page.waitForTimeout(500);
  272. }
  273. // ===== 订单 CRUD 操作 =====
  274. /**
  275. * 打开创建订单对话框
  276. */
  277. async openCreateDialog() {
  278. await this.addOrderButton.click();
  279. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  280. }
  281. /**
  282. * 打开编辑订单对话框
  283. * @param orderName 订单名称
  284. */
  285. async openEditDialog(orderName: string) {
  286. // 找到订单行并点击"打开菜单"按钮
  287. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  288. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  289. await menuButton.click();
  290. // 等待菜单出现并点击"编辑"选项
  291. // 使用 data-testid 或 role 定位编辑选项
  292. const editOption = this.page.getByRole('menuitem', { name: '编辑' });
  293. await editOption.waitFor({ state: 'visible', timeout: 3000 });
  294. await editOption.click();
  295. // 等待编辑对话框出现
  296. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  297. }
  298. /**
  299. * 打开删除确认对话框
  300. * @param orderName 订单名称
  301. */
  302. async openDeleteDialog(orderName: string) {
  303. // 找到订单行并点击"打开菜单"按钮(与编辑操作相同的模式)
  304. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  305. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  306. await menuButton.click();
  307. // 等待菜单出现并点击"删除"选项
  308. const deleteOption = this.page.getByRole('menuitem', { name: '删除' });
  309. await deleteOption.waitFor({ state: 'visible', timeout: 3000 });
  310. await deleteOption.click();
  311. // 等待删除确认对话框出现
  312. await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
  313. }
  314. /**
  315. * 填写订单表单
  316. * @param data 订单数据
  317. */
  318. async fillOrderForm(data: OrderData) {
  319. // 等待表单出现
  320. await this.page.waitForSelector('form', { state: 'visible', timeout: 5000 });
  321. // 填写订单名称
  322. if (data.name) {
  323. await this.page.getByLabel(/订单名称|名称/).fill(data.name);
  324. }
  325. // 填写预计开始日期
  326. if (data.expectedStartDate) {
  327. const dateInput = this.page.getByLabel(/预计开始日期|开始日期/);
  328. await dateInput.fill(data.expectedStartDate);
  329. }
  330. // 选择平台
  331. if (data.platformName) {
  332. await selectRadixOption(this.page, '平台', data.platformName);
  333. }
  334. // 选择公司
  335. if (data.companyName) {
  336. await selectRadixOption(this.page, '公司', data.companyName);
  337. }
  338. // 选择渠道
  339. if (data.channelName) {
  340. await selectRadixOption(this.page, '渠道', data.channelName);
  341. }
  342. // 选择订单状态(如果是编辑模式)
  343. if (data.status) {
  344. const statusLabel = ORDER_STATUS_LABELS[data.status];
  345. await selectRadixOption(this.page, '订单状态', statusLabel);
  346. }
  347. // 选择工作状态(如果是编辑模式)
  348. if (data.workStatus) {
  349. const workStatusLabel = WORK_STATUS_LABELS[data.workStatus];
  350. await selectRadixOption(this.page, '工作状态', workStatusLabel);
  351. }
  352. }
  353. /**
  354. * 提交表单
  355. * @returns 表单提交结果
  356. */
  357. async submitForm(): Promise<FormSubmitResult> {
  358. // 收集网络响应
  359. const responses: NetworkResponse[] = [];
  360. // 监听所有网络请求
  361. const responseHandler = async (response: Response) => {
  362. const url = response.url();
  363. // 监听订单管理相关的 API 请求
  364. if (url.includes('/orders') || url.includes('order')) {
  365. const requestBody = response.request()?.postData();
  366. const responseBody = await response.text().catch(() => '');
  367. let jsonBody = null;
  368. try {
  369. jsonBody = JSON.parse(responseBody);
  370. } catch {
  371. // 不是 JSON 响应
  372. }
  373. responses.push({
  374. url,
  375. method: response.request()?.method() ?? 'UNKNOWN',
  376. status: response.status(),
  377. ok: response.ok(),
  378. responseHeaders: await response.allHeaders().catch(() => ({})),
  379. responseBody: jsonBody || responseBody,
  380. });
  381. }
  382. };
  383. this.page.on('response', responseHandler);
  384. try {
  385. // 点击提交按钮(创建或更新)
  386. const submitButton = this.page.getByRole('button', { name: /^(创建|更新|保存)$/ });
  387. await submitButton.click();
  388. // 等待网络请求完成(使用较宽松的超时,因为有些操作可能不触发网络请求)
  389. try {
  390. await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 });
  391. } catch {
  392. // domcontentloaded 超时不是致命错误,继续检查 Toast 消息
  393. console.debug('domcontentloaded 超时,继续检查 Toast 消息');
  394. }
  395. } finally {
  396. // 确保监听器总是被移除,防止内存泄漏
  397. this.page.off('response', responseHandler);
  398. }
  399. // 等待 Toast 消息显示
  400. await this.page.waitForTimeout(2000);
  401. // 检查 Toast 消息
  402. const errorToast = this.page.locator('[data-sonner-toast][data-type="error"]');
  403. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  404. const hasError = await errorToast.count() > 0;
  405. const hasSuccess = await successToast.count() > 0;
  406. let errorMessage: string | null = null;
  407. let successMessage: string | null = null;
  408. if (hasError) {
  409. errorMessage = await errorToast.first().textContent();
  410. }
  411. if (hasSuccess) {
  412. successMessage = await successToast.first().textContent();
  413. }
  414. return {
  415. success: hasSuccess || (!hasError && !hasSuccess),
  416. hasError,
  417. hasSuccess,
  418. errorMessage: errorMessage ?? undefined,
  419. successMessage: successMessage ?? undefined,
  420. responses,
  421. };
  422. }
  423. /**
  424. * 取消对话框
  425. */
  426. async cancelDialog() {
  427. const cancelButton = this.page.getByRole('button', { name: '取消' });
  428. await cancelButton.click();
  429. await this.waitForDialogClosed();
  430. }
  431. /**
  432. * 等待对话框关闭
  433. */
  434. async waitForDialogClosed() {
  435. // 先等待一段时间让对话框有机会关闭
  436. await this.page.waitForTimeout(1000);
  437. // 检查是否还有对话框可见
  438. const dialogs = this.page.locator('[role="dialog"]');
  439. const dialogCount = await dialogs.count();
  440. if (dialogCount === 0) {
  441. // 没有对话框了,已经关闭
  442. console.debug('对话框已关闭(无对话框元素)');
  443. return;
  444. }
  445. // 尝试等待对话框隐藏或从 DOM 中移除
  446. try {
  447. await dialogs.first().waitFor({ state: 'hidden', timeout: 3000 });
  448. console.debug('对话框已关闭');
  449. } catch {
  450. // 超时不是致命错误,对话框可能已经以其他方式关闭
  451. console.debug('对话框关闭等待超时,继续执行');
  452. }
  453. await this.page.waitForTimeout(500);
  454. }
  455. /**
  456. * 确认删除操作
  457. */
  458. async confirmDelete() {
  459. // 尝试多种可能的按钮名称
  460. const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', {
  461. name: /^(确认删除|删除|确定|确认)$/
  462. });
  463. await confirmButton.click();
  464. // 等待确认对话框关闭和网络请求完成
  465. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  466. .catch(() => console.debug('删除确认对话框关闭超时'));
  467. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  468. await this.page.waitForTimeout(1000);
  469. }
  470. /**
  471. * 取消删除操作
  472. */
  473. async cancelDelete() {
  474. const cancelButton = this.page.getByRole('button', { name: '取消' }).and(
  475. this.page.locator('[role="alertdialog"]')
  476. );
  477. await cancelButton.click();
  478. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  479. .catch(() => console.debug('删除确认对话框关闭超时(取消操作)'));
  480. }
  481. /**
  482. * 验证订单是否存在
  483. * @param orderName 订单名称
  484. * @returns 订单是否存在
  485. */
  486. async orderExists(orderName: string): Promise<boolean> {
  487. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  488. return (await orderRow.count()) > 0;
  489. }
  490. // ===== 订单详情 =====
  491. /**
  492. * 打开订单详情对话框
  493. * @param orderName 订单名称
  494. */
  495. async openDetailDialog(orderName: string) {
  496. // 找到订单行
  497. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  498. // 先点击操作菜单触发按钮("打开菜单" 或 MoreHorizontal 图标)
  499. const menuTrigger = orderRow.getByRole('button', { name: /打开菜单/ });
  500. await menuTrigger.click();
  501. // 等待菜单显示
  502. await this.page.waitForTimeout(200);
  503. // 点击"查看详情"菜单项
  504. const detailButton = this.page.getByRole('menuitem', { name: /查看详情/ });
  505. await detailButton.click();
  506. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  507. }
  508. /**
  509. * 获取订单详情中的基本信息
  510. * @returns 订单基本信息
  511. */
  512. async getOrderDetailInfo(): Promise<{
  513. name?: string;
  514. status?: string;
  515. workStatus?: string;
  516. expectedStartDate?: string;
  517. platform?: string;
  518. company?: string;
  519. channel?: string;
  520. }> {
  521. const dialog = this.page.locator('[role="dialog"]');
  522. const result: Record<string, string | undefined> = {};
  523. // 辅助函数:通过标签文本获取对应值
  524. // DOM 结构: generic (field row) > generic (label) + generic (value)
  525. const getValueByLabel = async (labelText: string | RegExp): Promise<string | undefined> => {
  526. const label = dialog.locator('generic').filter({ hasText: labelText }).first();
  527. if (await label.count() > 0) {
  528. const value = label.locator('..').locator('generic').nth(1);
  529. if (await value.count() > 0) {
  530. const text = await value.textContent();
  531. return text?.trim() || undefined;
  532. }
  533. }
  534. return undefined;
  535. };
  536. // 订单名称 - 查找"订单名称:"标签后的值
  537. result.name = await getValueByLabel('订单名称:');
  538. // 订单状态
  539. result.status = await getValueByLabel('订单状态:');
  540. // 工作状态
  541. result.workStatus = await getValueByLabel('工作状态:');
  542. // 预计开始日期
  543. result.expectedStartDate = await getValueByLabel(/预计开始日期:|开始日期:/);
  544. // 平台
  545. result.platform = await getValueByLabel('平台:');
  546. // 公司
  547. result.company = await getValueByLabel('公司:');
  548. // 渠道
  549. result.channel = await getValueByLabel('渠道:');
  550. return result;
  551. }
  552. /**
  553. * 从订单详情对话框中获取关联人员列表
  554. * @returns 人员信息列表
  555. */
  556. async getPersonListFromDetail(): Promise<Array<{
  557. name?: string;
  558. workStatus?: string;
  559. hireDate?: string;
  560. salary?: string;
  561. }>> {
  562. const dialog = this.page.locator('[role="dialog"]');
  563. const result: Array<{ name?: string; workStatus?: string; hireDate?: string; salary?: string }> = [];
  564. // 查找所有表格,对话框中可能有两个表格:
  565. // 1. "待添加人员列表" - 临时表格,包含未确认的人员
  566. // 2. "绑定人员列表" - 实际已绑定到订单的人员
  567. // 我们需要第二个"绑定人员列表"表格
  568. const allTables = dialog.locator('table');
  569. const tableCount = await allTables.count();
  570. // 查找"绑定人员列表"表格(通常是包含"工作状态"列的表格)
  571. let personTable;
  572. for (let i = 0; i < tableCount; i++) {
  573. const table = allTables.nth(i);
  574. const tableText = await table.textContent();
  575. // 绑定人员列表表格包含"工作状态"列,而待添加人员列表没有
  576. if (tableText && tableText.includes('工作状态')) {
  577. personTable = table;
  578. break;
  579. }
  580. }
  581. const personList = dialog.locator('[class*="person"], [class*="employee"], [data-testid*="person"]');
  582. // 优先使用表格形式
  583. if (personTable) {
  584. const rows = personTable.locator('tbody tr');
  585. const rowCount = await rows.count();
  586. for (let i = 0; i < rowCount; i++) {
  587. const row = rows.nth(i);
  588. const cells = row.locator('td');
  589. const cellCount = await cells.count();
  590. const personInfo: { name?: string; workStatus?: string; hireDate?: string; salary?: string } = {};
  591. // 根据列数量和数据类型提取信息
  592. for (let j = 0; j < cellCount; j++) {
  593. const cellText = await cells.nth(j).textContent();
  594. if (!cellText) continue;
  595. const trimmedText = cellText.trim();
  596. // 尝试识别列内容
  597. // 姓名通常在第一列
  598. if (j === 0 && trimmedText) {
  599. personInfo.name = trimmedText;
  600. }
  601. // 工作状态检查
  602. for (const [statusValue, statusLabel] of Object.entries(WORK_STATUS_LABELS)) {
  603. if (trimmedText.includes(statusLabel)) {
  604. personInfo.workStatus = statusLabel;
  605. break;
  606. }
  607. }
  608. // 薪资检查(包含数字)
  609. if (/^\d+(\.\d+)?$/.test(trimmedText.replace(/,/g, ''))) {
  610. personInfo.salary = trimmedText;
  611. }
  612. // 日期检查(符合日期格式)
  613. if (/^\d{4}-\d{2}-\d{2}$/.test(trimmedText) || /^\d{4}\/\d{2}\/\d{2}$/.test(trimmedText)) {
  614. if (!personInfo.hireDate) {
  615. personInfo.hireDate = trimmedText;
  616. }
  617. }
  618. }
  619. if (personInfo.name || personInfo.workStatus) {
  620. result.push(personInfo);
  621. }
  622. }
  623. } else if (await personList.count() > 0) {
  624. // 如果是列表形式而非表格
  625. const listItems = personList.locator('[class*="item"], [class*="row"], li, div');
  626. const itemCount = await listItems.count();
  627. for (let i = 0; i < itemCount; i++) {
  628. const item = listItems.nth(i);
  629. const itemText = await item.textContent();
  630. if (itemText && itemText.trim()) {
  631. result.push({ name: itemText.trim() });
  632. }
  633. }
  634. }
  635. return result;
  636. }
  637. /**
  638. * 从订单详情对话框中获取附件列表
  639. * @returns 附件信息列表
  640. */
  641. async getAttachmentListFromDetail(): Promise<Array<{
  642. fileName?: string;
  643. uploadDate?: string;
  644. uploader?: string;
  645. }>> {
  646. const dialog = this.page.locator('[role="dialog"]');
  647. const result: Array<{ fileName?: string; uploadDate?: string; uploader?: string }> = [];
  648. // 查找附件列表区域
  649. // 尝试多种可能的定位策略
  650. const attachmentTable = dialog.locator('table').filter({ hasText: /附件|文件/ });
  651. const attachmentList = dialog.locator('[class*="attachment"], [class*="file"], [data-testid*="attachment"]');
  652. // 优先使用表格形式
  653. if (await attachmentTable.count() > 0) {
  654. const rows = attachmentTable.locator('tbody tr');
  655. const rowCount = await rows.count();
  656. for (let i = 0; i < rowCount; i++) {
  657. const row = rows.nth(i);
  658. const cells = row.locator('td');
  659. const cellCount = await cells.count();
  660. const attachmentInfo: { fileName?: string; uploadDate?: string; uploader?: string } = {};
  661. for (let j = 0; j < cellCount; j++) {
  662. const cellText = await cells.nth(j).textContent();
  663. if (!cellText) continue;
  664. const trimmedText = cellText.trim();
  665. // 文件名通常在第一列
  666. if (j === 0 && trimmedText) {
  667. attachmentInfo.fileName = trimmedText;
  668. }
  669. // 日期检查
  670. if (/^\d{4}-\d{2}-\d{2}/.test(trimmedText) || /^\d{4}\/\d{2}\/\d{2}/.test(trimmedText)) {
  671. if (!attachmentInfo.uploadDate) {
  672. attachmentInfo.uploadDate = trimmedText;
  673. }
  674. }
  675. // 上传者通常是文本用户名
  676. if (j > 0 && trimmedText && !attachmentInfo.uploader && !attachmentInfo.uploadDate && !/^\d{4}/.test(trimmedText)) {
  677. attachmentInfo.uploader = trimmedText;
  678. }
  679. }
  680. if (attachmentInfo.fileName) {
  681. result.push(attachmentInfo);
  682. }
  683. }
  684. } else if (await attachmentList.count() > 0) {
  685. // 如果是列表形式
  686. const listItems = attachmentList.locator('[class*="item"], [class*="row"], li, div');
  687. const itemCount = await listItems.count();
  688. for (let i = 0; i < itemCount; i++) {
  689. const item = listItems.nth(i);
  690. const itemText = await item.textContent();
  691. if (itemText && itemText.trim()) {
  692. result.push({ fileName: itemText.trim() });
  693. }
  694. }
  695. }
  696. return result;
  697. }
  698. /**
  699. * 关闭订单详情对话框
  700. */
  701. async closeDetailDialog(): Promise<void> {
  702. // 尝试多种关闭方式
  703. // 方式1: 点击右上角 X 按钮
  704. const closeButton = this.page.locator('[role="dialog"]').getByRole('button', { name: '关闭' }).first();
  705. const closeButtonCount = await closeButton.count();
  706. if (closeButtonCount > 0) {
  707. await closeButton.click();
  708. } else {
  709. // 方式2: 点击取消按钮
  710. const cancelButton = this.page.locator('[role="dialog"]').getByRole('button', { name: '取消' }).first();
  711. const cancelButtonCount = await cancelButton.count();
  712. if (cancelButtonCount > 0) {
  713. await cancelButton.click();
  714. } else {
  715. // 方式3: 按 Escape 键
  716. await this.page.keyboard.press('Escape');
  717. }
  718. }
  719. // 等待对话框关闭
  720. await this.waitForDialogClosed();
  721. }
  722. // ===== 人员关联管理 =====
  723. /**
  724. * 打开人员管理对话框
  725. *
  726. * **使用场景:**
  727. * - **从订单列表页打开**: 传入 `orderName` 参数,方法会先找到对应订单行,再点击人员管理按钮
  728. * - **从订单详情页打开**: 不传参数,方法会直接点击页面中的人员管理按钮
  729. *
  730. * @param orderName 订单名称(可选)。从列表页打开时需要传入,从详情页打开时不传
  731. *
  732. * @example
  733. * ```typescript
  734. * // 从订单列表页打开
  735. * await orderPage.openPersonManagementDialog('测试订单');
  736. *
  737. * // 从订单详情页打开
  738. * await orderPage.openDetailDialog('测试订单');
  739. * await orderPage.openPersonManagementDialog();
  740. * ```
  741. */
  742. async openPersonManagementDialog(orderName?: string) {
  743. // 人员管理功能直接集成在订单详情对话框中
  744. // 如果提供了订单名称,打开订单详情对话框
  745. if (orderName) {
  746. await this.openDetailDialog(orderName);
  747. }
  748. // 人员管理功能已在详情对话框中,无需额外操作
  749. }
  750. /**
  751. * 添加人员到订单
  752. * @param personData 人员数据
  753. */
  754. async addPersonToOrder(personData: OrderPersonData) {
  755. // 点击添加人员按钮
  756. const addButton = this.page.getByRole('button', { name: /添加人员|新增人员/ });
  757. await addButton.click();
  758. await this.page.waitForTimeout(300);
  759. // 选择残疾人(支持通过名称选择)
  760. if (personData.disabledPersonName) {
  761. await selectRadixOption(this.page, '残疾人|选择残疾人', personData.disabledPersonName);
  762. } else if (personData.disabledPersonId) {
  763. // 如果只提供了 ID,尝试在对话框中选择第一个残疾人
  764. const firstCheckbox = this.page.locator('[role="dialog"]').locator('table tbody tr').first().locator('input[type="checkbox"]').first();
  765. try {
  766. await firstCheckbox.waitFor({ state: 'visible', timeout: 3000 });
  767. await firstCheckbox.check();
  768. } catch {
  769. console.debug('没有可用的残疾人数据');
  770. }
  771. }
  772. // 填写入职日期
  773. if (personData.hireDate) {
  774. const hireDateInput = this.page.getByLabel(/入职日期/);
  775. await hireDateInput.fill(personData.hireDate);
  776. }
  777. // 填写薪资
  778. if (personData.salary !== undefined) {
  779. const salaryInput = this.page.getByLabel(/薪资|工资/);
  780. await salaryInput.fill(String(personData.salary));
  781. }
  782. // 选择工作状态
  783. if (personData.workStatus) {
  784. const workStatusLabel = WORK_STATUS_LABELS[personData.workStatus];
  785. await selectRadixOption(this.page, '工作状态', workStatusLabel);
  786. }
  787. // 提交
  788. const submitButton = this.page.getByRole('button', { name: /^(添加|确定|保存)$/ });
  789. await submitButton.click();
  790. await this.page.waitForLoadState('networkidle');
  791. await this.page.waitForTimeout(1000);
  792. }
  793. /**
  794. * 修改人员工作状态
  795. * @param personName 人员姓名
  796. * @param newStatus 新的工作状态
  797. */
  798. async updatePersonWorkStatus(personName: string, newStatus: WorkStatus) {
  799. const dialog = this.page.locator('[role="dialog"]');
  800. // 等待对话框完全加载
  801. await dialog.waitFor({ state: 'visible', timeout: 5000 });
  802. // 从 error-context.md 可知:
  803. // 1. 对话框中有"绑定人员列表"表格
  804. // 2. 表格列:ID 姓名 性别 残疾类型 联系电话 入职日期 离职日期 工作状态 薪资
  805. // 3. 工作状态列直接是 combobox,不需要点击编辑按钮
  806. // 查找所有表格
  807. const allTables = dialog.locator('table');
  808. const allTableCount = await allTables.count();
  809. console.debug(`对话框中总共有 ${allTableCount} 个表格`);
  810. let personTable = allTables.first();
  811. // 找到包含"绑定人员"或"工作状态"列的表格(第二个表格是绑定人员列表)
  812. for (let i = 0; i < allTableCount; i++) {
  813. const table = allTables.nth(i);
  814. const tableText = await table.textContent();
  815. if (tableText && (tableText.includes('绑定人员') || tableText.includes('工作状态'))) {
  816. personTable = table;
  817. console.debug(`找到人员表格(索引 ${i})`);
  818. break;
  819. }
  820. }
  821. // 在表格中查找包含指定人员名称的行
  822. const targetRow = personTable.locator('tbody tr').filter({ hasText: personName }).first();
  823. const rowCount = await targetRow.count();
  824. console.debug(`找到 ${rowCount} 个匹配的人员行`);
  825. if (rowCount === 0) {
  826. throw new Error(`未找到人员 ${personName}`);
  827. }
  828. // 等待行可见
  829. await targetRow.waitFor({ state: 'visible', timeout: 5000 });
  830. // 从 error-context.md 可知,工作状态在单元格中是一个 combobox
  831. // 表格列:ID 姓名 性别 残疾类型 联系电话 入职日期 离职日期 工作状态 薪资
  832. // 工作状态是倒数第二列(薪资是最后一列)
  833. const cells = targetRow.locator('td');
  834. const cellCount = await cells.count();
  835. console.debug(`人员行有 ${cellCount} 个单元格`);
  836. // 工作状态在倒数第二列
  837. const workStatusCell = cells.nth(cellCount - 2);
  838. const workStatusCombobox = workStatusCell.getByRole('combobox');
  839. const comboboxCount = await workStatusCombobox.count();
  840. console.debug(`工作状态 combobox 数量: ${comboboxCount}`);
  841. if (comboboxCount === 0) {
  842. throw new Error(`未找到人员 ${personName} 的工作状态选择器`);
  843. }
  844. await workStatusCombobox.click({ timeout: 5000 });
  845. console.debug('工作状态 combobox 已点击');
  846. // 等待下拉选项显示
  847. await this.page.waitForTimeout(500);
  848. // 使用中文标签选择选项
  849. // 注意:UI 中的工作状态选项与 WORK_STATUS_LABELS 不同
  850. // UI 选项:未入职、已入职、工作中、已离职
  851. // WORK_STATUS_LABELS:未就业、待就业、已就业、已离职
  852. const statusMapping: Record<WorkStatus, string> = {
  853. not_working: '未入职',
  854. pre_working: '已入职',
  855. working: '工作中',
  856. resigned: '已离职',
  857. };
  858. const newWorkStatusLabel = statusMapping[newStatus];
  859. console.debug(`尝试选择状态: ${newWorkStatusLabel}`);
  860. const optionLocator = this.page.getByRole('option', { name: newWorkStatusLabel });
  861. const optionCount = await optionLocator.count();
  862. console.debug(`找到 ${optionCount} 个选项`);
  863. if (optionCount === 0) {
  864. throw new Error(`未找到工作状态选项: ${newWorkStatusLabel}`);
  865. }
  866. await optionLocator.first().click({ timeout: 5000 });
  867. console.debug(`工作状态已更新为: ${newWorkStatusLabel}`);
  868. // 使用较短的超时时间等待网络空闲
  869. await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 })
  870. .catch(() => console.debug('domcontentloaded 等待超时,继续'));
  871. await this.page.waitForTimeout(500);
  872. }
  873. // ===== 附件管理 =====
  874. /**
  875. * 打开添加附件对话框
  876. */
  877. async openAddAttachmentDialog() {
  878. const attachmentButton = this.page.getByRole('button', { name: /添加附件|上传附件/ });
  879. await attachmentButton.click();
  880. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  881. }
  882. /**
  883. * 上传附件
  884. * @param personName 人员姓名
  885. * @param fileName 文件名
  886. * @param mimeType 文件类型(默认为 image/jpeg)
  887. */
  888. async uploadAttachment(personName: string, fileName: string, mimeType: string = 'image/jpeg') {
  889. // 选择订单人员
  890. const personSelect = this.page.getByLabel(/选择人员|订单人员/);
  891. await personSelect.click();
  892. await this.page.getByRole('option', { name: personName }).click();
  893. // 查找文件上传输入框
  894. const fileInput = this.page.locator('input[type="file"]');
  895. await fileInput.setInputFiles({
  896. name: fileName,
  897. mimeType,
  898. buffer: Buffer.from(`fake ${fileName} content`),
  899. });
  900. // 等待上传处理
  901. await this.page.waitForTimeout(500);
  902. // 提交
  903. const submitButton = this.page.getByRole('button', { name: /^(上传|确定|保存)$/ });
  904. await submitButton.click();
  905. await this.page.waitForLoadState('networkidle');
  906. await this.page.waitForTimeout(1000);
  907. }
  908. // ===== 高级操作 =====
  909. /**
  910. * 创建订单(完整流程)
  911. * @param data 订单数据
  912. * @returns 表单提交结果
  913. */
  914. async createOrder(data: OrderData): Promise<FormSubmitResult> {
  915. await this.openCreateDialog();
  916. await this.fillOrderForm(data);
  917. const result = await this.submitForm();
  918. await this.waitForDialogClosed();
  919. return result;
  920. }
  921. /**
  922. * 编辑订单(完整流程)
  923. * @param orderName 订单名称
  924. * @param data 更新的订单数据
  925. * @returns 表单提交结果
  926. */
  927. async editOrder(orderName: string, data: OrderData): Promise<FormSubmitResult> {
  928. await this.openEditDialog(orderName);
  929. await this.fillOrderForm(data);
  930. const result = await this.submitForm();
  931. await this.waitForDialogClosed();
  932. return result;
  933. }
  934. /**
  935. * 删除订单(完整流程)
  936. * @param orderName 订单名称
  937. * @returns 是否成功删除
  938. */
  939. async deleteOrder(orderName: string): Promise<boolean> {
  940. await this.openDeleteDialog(orderName);
  941. await this.confirmDelete();
  942. // 等待并检查 Toast 消息
  943. await this.page.waitForTimeout(1000);
  944. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  945. const hasSuccess = await successToast.count() > 0;
  946. return hasSuccess;
  947. }
  948. // ===== 订单状态流转操作 =====
  949. /**
  950. * 打开激活订单确认对话框
  951. * @param orderName 订单名称
  952. */
  953. async openActivateDialog(orderName: string): Promise<void> {
  954. // 找到订单行并点击"打开菜单"按钮(与编辑/删除操作相同的模式)
  955. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  956. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  957. await menuButton.click();
  958. // 等待菜单出现并点击"激活"选项
  959. const activateOption = this.page.getByRole('menuitem', { name: /激活|激活订单/ });
  960. await activateOption.waitFor({ state: 'visible', timeout: 3000 });
  961. await activateOption.click();
  962. // 等待确认对话框出现
  963. await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
  964. }
  965. /**
  966. * 确认激活订单
  967. */
  968. async confirmActivate(): Promise<void> {
  969. // 尝试多种可能的按钮名称
  970. const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', {
  971. name: /^(确认激活|激活|确定|确认)$/
  972. });
  973. await confirmButton.click();
  974. // 等待确认对话框关闭和网络请求完成
  975. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  976. .catch(() => console.debug('激活确认对话框关闭超时'));
  977. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  978. await this.page.waitForTimeout(1000);
  979. }
  980. /**
  981. * 激活订单(完整流程)
  982. * @param orderName 订单名称
  983. * @returns 是否成功激活
  984. */
  985. async activateOrder(orderName: string): Promise<boolean> {
  986. await this.openActivateDialog(orderName);
  987. await this.confirmActivate();
  988. // 等待并检查 Toast 消息
  989. await this.page.waitForTimeout(1000);
  990. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  991. const hasSuccess = await successToast.count() > 0;
  992. return hasSuccess;
  993. }
  994. /**
  995. * 打开关闭订单确认对话框
  996. * @param orderName 订单名称
  997. */
  998. async openCloseDialog(orderName: string): Promise<void> {
  999. // 找到订单行并点击"打开菜单"按钮
  1000. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  1001. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  1002. await menuButton.click();
  1003. // 等待菜单出现并点击"关闭"选项
  1004. const closeOption = this.page.getByRole('menuitem', { name: /关闭|关闭订单|完成/ });
  1005. await closeOption.waitFor({ state: 'visible', timeout: 3000 });
  1006. await closeOption.click();
  1007. // 等待确认对话框出现
  1008. await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
  1009. }
  1010. /**
  1011. * 确认关闭订单
  1012. */
  1013. async confirmClose(): Promise<void> {
  1014. // 尝试多种可能的按钮名称
  1015. const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', {
  1016. name: /^(确认关闭|关闭|确定|确认)$/
  1017. });
  1018. await confirmButton.click();
  1019. // 等待确认对话框关闭和网络请求完成
  1020. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  1021. .catch(() => console.debug('关闭确认对话框关闭超时'));
  1022. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  1023. await this.page.waitForTimeout(1000);
  1024. }
  1025. /**
  1026. * 关闭订单(完整流程)
  1027. * @param orderName 订单名称
  1028. * @returns 是否成功关闭
  1029. */
  1030. async closeOrder(orderName: string): Promise<boolean> {
  1031. await this.openCloseDialog(orderName);
  1032. await this.confirmClose();
  1033. // 等待并检查 Toast 消息
  1034. await this.page.waitForTimeout(1000);
  1035. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  1036. const hasSuccess = await successToast.count() > 0;
  1037. return hasSuccess;
  1038. }
  1039. /**
  1040. * 获取订单的当前状态(从列表页面)
  1041. * @param orderName 订单名称
  1042. * @returns 订单状态值或 null
  1043. */
  1044. async getOrderStatus(orderName: string): Promise<OrderStatus | null> {
  1045. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  1046. // 等待行可见
  1047. await orderRow.waitFor({ state: 'visible', timeout: 3000 }).catch(() => {
  1048. console.debug(`订单 "${orderName}" 行不可见`);
  1049. });
  1050. const rowCount = await orderRow.count();
  1051. if (rowCount === 0) {
  1052. console.debug(`订单 "${orderName}" 不存在`);
  1053. return null;
  1054. }
  1055. // 尝试多种策略定位状态列
  1056. // 策略1: 查找包含状态文本的单元格(但排除订单名称列)
  1057. const allCells = orderRow.locator('td');
  1058. const cellCount = await allCells.count();
  1059. for (let i = 1; i < cellCount; i++) { // 跳过第一列(通常是订单名称)
  1060. const cell = allCells.nth(i);
  1061. const cellText = await cell.textContent();
  1062. if (cellText) {
  1063. // 检查是否包含完整的状态标签(避免部分匹配)
  1064. for (const [statusValue, statusLabel] of Object.entries(ORDER_STATUS_LABELS)) {
  1065. // 使用更严格的匹配:必须是状态标签本身或包含完整标签
  1066. const trimmedText = cellText.trim();
  1067. if (trimmedText === statusLabel || trimmedText.includes(`${statusLabel}`)) {
  1068. // 验证不是订单名称列(额外检查)
  1069. const firstCellText = await allCells.nth(0).textContent();
  1070. if (firstCellText && !firstCellText.includes(orderName.substring(0, 3))) {
  1071. // 第一列不包含订单名称开头,说明列结构可能不同
  1072. return statusValue as OrderStatus;
  1073. }
  1074. // 跳过第一列后找到的状态标签才返回
  1075. return statusValue as OrderStatus;
  1076. }
  1077. }
  1078. }
  1079. }
  1080. // 策略2: 如果上述方法失败,尝试查找状态徽章/标签元素
  1081. // 查找具有状态样式特征的元素
  1082. const statusBadge = orderRow.locator('[class*="status"], [class*="badge"], span').filter({
  1083. hasText: Object.values(ORDER_STATUS_LABELS)
  1084. });
  1085. if (await statusBadge.count() > 0) {
  1086. const badgeText = await statusBadge.first().textContent();
  1087. if (badgeText) {
  1088. for (const [statusValue, statusLabel] of Object.entries(ORDER_STATUS_LABELS)) {
  1089. if (badgeText.includes(statusLabel)) {
  1090. return statusValue as OrderStatus;
  1091. }
  1092. }
  1093. }
  1094. }
  1095. console.debug(`无法从订单 "${orderName}" 中解析状态`);
  1096. return null;
  1097. }
  1098. /**
  1099. * 验证订单状态
  1100. * @param orderName 订单名称
  1101. * @param expectedStatus 期望的状态
  1102. */
  1103. async expectOrderStatus(orderName: string, expectedStatus: OrderStatus): Promise<void> {
  1104. const actualStatus = await this.getOrderStatus(orderName);
  1105. if (actualStatus === null) {
  1106. throw new Error(`订单 "${orderName}" 未找到或状态列无法识别`);
  1107. }
  1108. if (actualStatus !== expectedStatus) {
  1109. throw new Error(
  1110. `订单 "${orderName}" 状态不匹配: 期望 "${ORDER_STATUS_LABELS[expectedStatus]}", 实际 "${ORDER_STATUS_LABELS[actualStatus]}"`
  1111. );
  1112. }
  1113. }
  1114. /**
  1115. * 检查激活按钮是否可用
  1116. *
  1117. * **注意**: 此方法会打开和关闭菜单,属于有副作用的操作
  1118. *
  1119. * @param orderName 订单名称
  1120. * @returns 按钮是否可用
  1121. */
  1122. async checkActivateButtonEnabled(orderName: string): Promise<boolean> {
  1123. // 找到订单行并打开菜单
  1124. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  1125. // 检查订单是否存在
  1126. const orderCount = await orderRow.count();
  1127. if (orderCount === 0) {
  1128. console.debug(`订单 "${orderName}" 不存在`);
  1129. return false;
  1130. }
  1131. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  1132. try {
  1133. await menuButton.click();
  1134. } catch (error) {
  1135. console.debug(`无法打开订单 "${orderName}" 的菜单:`, error);
  1136. return false;
  1137. }
  1138. // 检查激活菜单项是否可点击
  1139. const activateOption = this.page.getByRole('menuitem', { name: /激活|激活订单/ });
  1140. const isVisible = await activateOption.isVisible().catch(() => false);
  1141. let isEnabled = false;
  1142. if (isVisible) {
  1143. // 检查是否有禁用属性或样式
  1144. const isDisabled = await activateOption.isDisabled().catch(() => false);
  1145. isEnabled = !isDisabled;
  1146. }
  1147. // 关闭菜单以便后续操作
  1148. await this.page.keyboard.press('Escape');
  1149. await this.page.waitForTimeout(300);
  1150. return isEnabled;
  1151. }
  1152. /**
  1153. * 检查关闭按钮是否可用
  1154. *
  1155. * **注意**: 此方法会打开和关闭菜单,属于有副作用的操作
  1156. *
  1157. * @param orderName 订单名称
  1158. * @returns 按钮是否可用
  1159. */
  1160. async checkCloseButtonEnabled(orderName: string): Promise<boolean> {
  1161. // 找到订单行并打开菜单
  1162. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  1163. // 检查订单是否存在
  1164. const orderCount = await orderRow.count();
  1165. if (orderCount === 0) {
  1166. console.debug(`订单 "${orderName}" 不存在`);
  1167. return false;
  1168. }
  1169. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  1170. try {
  1171. await menuButton.click();
  1172. } catch (error) {
  1173. console.debug(`无法打开订单 "${orderName}" 的菜单:`, error);
  1174. return false;
  1175. }
  1176. // 检查关闭菜单项是否可点击
  1177. const closeOption = this.page.getByRole('menuitem', { name: /关闭|关闭订单|完成/ });
  1178. const isVisible = await closeOption.isVisible().catch(() => false);
  1179. let isEnabled = false;
  1180. if (isVisible) {
  1181. // 检查是否有禁用属性或样式
  1182. const isDisabled = await closeOption.isDisabled().catch(() => false);
  1183. isEnabled = !isDisabled;
  1184. }
  1185. // 关闭菜单以便后续操作
  1186. await this.page.keyboard.press('Escape');
  1187. await this.page.waitForTimeout(300);
  1188. return isEnabled;
  1189. }
  1190. }