order-management.page.ts 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290
  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_EMPLOYED: 'not_employed',
  30. PENDING: 'pending',
  31. EMPLOYED: 'employed',
  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_employed: '未就业',
  43. pending: '待就业',
  44. employed: '已就业',
  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('networkidle', { timeout: 5000 });
  391. } catch {
  392. // networkidle 超时不是致命错误,继续检查 Toast 消息
  393. console.debug('networkidle 超时,继续检查 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. const dialog = this.page.locator('[role="dialog"]');
  436. await dialog.waitFor({ state: 'hidden', timeout: 5000 })
  437. .catch(() => console.debug('对话框关闭超时,可能已经关闭'));
  438. await this.page.waitForTimeout(500);
  439. }
  440. /**
  441. * 确认删除操作
  442. */
  443. async confirmDelete() {
  444. // 尝试多种可能的按钮名称
  445. const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', {
  446. name: /^(确认删除|删除|确定|确认)$/
  447. });
  448. await confirmButton.click();
  449. // 等待确认对话框关闭和网络请求完成
  450. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  451. .catch(() => console.debug('删除确认对话框关闭超时'));
  452. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  453. await this.page.waitForTimeout(1000);
  454. }
  455. /**
  456. * 取消删除操作
  457. */
  458. async cancelDelete() {
  459. const cancelButton = this.page.getByRole('button', { name: '取消' }).and(
  460. this.page.locator('[role="alertdialog"]')
  461. );
  462. await cancelButton.click();
  463. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  464. .catch(() => console.debug('删除确认对话框关闭超时(取消操作)'));
  465. }
  466. /**
  467. * 验证订单是否存在
  468. * @param orderName 订单名称
  469. * @returns 订单是否存在
  470. */
  471. async orderExists(orderName: string): Promise<boolean> {
  472. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  473. return (await orderRow.count()) > 0;
  474. }
  475. // ===== 订单详情 =====
  476. /**
  477. * 打开订单详情对话框
  478. * @param orderName 订单名称
  479. */
  480. async openDetailDialog(orderName: string) {
  481. // 找到订单行并点击查看详情按钮
  482. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  483. const detailButton = orderRow.getByRole('button', { name: /详情|查看/ });
  484. await detailButton.click();
  485. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  486. }
  487. /**
  488. * 获取订单详情中的基本信息
  489. * @returns 订单基本信息
  490. */
  491. async getOrderDetailInfo(): Promise<{
  492. name?: string;
  493. status?: string;
  494. workStatus?: string;
  495. expectedStartDate?: string;
  496. platform?: string;
  497. company?: string;
  498. channel?: string;
  499. }> {
  500. const dialog = this.page.locator('[role="dialog"]');
  501. const result: Record<string, string | undefined> = {};
  502. // 订单名称 - 查找"订单名称"标签后的值
  503. const nameElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单名称' })
  504. .locator('..').locator('p,span,div').nth(1);
  505. if (await nameElement.count() > 0) {
  506. const text = await nameElement.textContent();
  507. result.name = text || undefined;
  508. }
  509. // 订单状态
  510. const statusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '订单状态' })
  511. .locator('..').locator('p,span,div').nth(1);
  512. if (await statusElement.count() > 0) {
  513. const text = await statusElement.textContent();
  514. result.status = text || undefined;
  515. }
  516. // 工作状态
  517. const workStatusElement = dialog.locator('.text-muted-foreground').filter({ hasText: '工作状态' })
  518. .locator('..').locator('p,span,div').nth(1);
  519. if (await workStatusElement.count() > 0) {
  520. const text = await workStatusElement.textContent();
  521. result.workStatus = text || undefined;
  522. }
  523. // 预计开始日期
  524. const startDateElement = dialog.locator('.text-muted-foreground').filter({ hasText: /预计开始日期|开始日期/ })
  525. .locator('..').locator('p,span,div').nth(1);
  526. if (await startDateElement.count() > 0) {
  527. const text = await startDateElement.textContent();
  528. result.expectedStartDate = text || undefined;
  529. }
  530. // 平台
  531. const platformElement = dialog.locator('.text-muted-foreground').filter({ hasText: '平台' })
  532. .locator('..').locator('p,span,div').nth(1);
  533. if (await platformElement.count() > 0) {
  534. const text = await platformElement.textContent();
  535. result.platform = text || undefined;
  536. }
  537. // 公司
  538. const companyElement = dialog.locator('.text-muted-foreground').filter({ hasText: '公司' })
  539. .locator('..').locator('p,span,div').nth(1);
  540. if (await companyElement.count() > 0) {
  541. const text = await companyElement.textContent();
  542. result.company = text || undefined;
  543. }
  544. // 渠道
  545. const channelElement = dialog.locator('.text-muted-foreground').filter({ hasText: '渠道' })
  546. .locator('..').locator('p,span,div').nth(1);
  547. if (await channelElement.count() > 0) {
  548. const text = await channelElement.textContent();
  549. result.channel = text || undefined;
  550. }
  551. return result;
  552. }
  553. /**
  554. * 从订单详情对话框中获取关联人员列表
  555. * @returns 人员信息列表
  556. */
  557. async getPersonListFromDetail(): Promise<Array<{
  558. name?: string;
  559. workStatus?: string;
  560. hireDate?: string;
  561. salary?: string;
  562. }>> {
  563. const dialog = this.page.locator('[role="dialog"]');
  564. const result: Array<{ name?: string; workStatus?: string; hireDate?: string; salary?: string }> = [];
  565. // 查找人员列表区域(通常在详情对话框中有一个表格或列表展示人员)
  566. // 尝试多种可能的定位策略
  567. const personTable = dialog.locator('table').filter({ hasText: /人员|员工/ });
  568. const personList = dialog.locator('[class*="person"], [class*="employee"], [data-testid*="person"]');
  569. // 优先使用表格形式
  570. if (await personTable.count() > 0) {
  571. const rows = personTable.locator('tbody tr');
  572. const rowCount = await rows.count();
  573. for (let i = 0; i < rowCount; i++) {
  574. const row = rows.nth(i);
  575. const cells = row.locator('td');
  576. const cellCount = await cells.count();
  577. const personInfo: { name?: string; workStatus?: string; hireDate?: string; salary?: string } = {};
  578. // 根据列数量和数据类型提取信息
  579. for (let j = 0; j < cellCount; j++) {
  580. const cellText = await cells.nth(j).textContent();
  581. if (!cellText) continue;
  582. const trimmedText = cellText.trim();
  583. // 尝试识别列内容
  584. // 姓名通常在第一列
  585. if (j === 0 && trimmedText) {
  586. personInfo.name = trimmedText;
  587. }
  588. // 工作状态检查
  589. for (const [statusValue, statusLabel] of Object.entries(WORK_STATUS_LABELS)) {
  590. if (trimmedText.includes(statusLabel)) {
  591. personInfo.workStatus = statusLabel;
  592. break;
  593. }
  594. }
  595. // 薪资检查(包含数字)
  596. if (/^\d+(\.\d+)?$/.test(trimmedText.replace(/,/g, ''))) {
  597. personInfo.salary = trimmedText;
  598. }
  599. // 日期检查(符合日期格式)
  600. if (/^\d{4}-\d{2}-\d{2}$/.test(trimmedText) || /^\d{4}\/\d{2}\/\d{2}$/.test(trimmedText)) {
  601. if (!personInfo.hireDate) {
  602. personInfo.hireDate = trimmedText;
  603. }
  604. }
  605. }
  606. if (personInfo.name || personInfo.workStatus) {
  607. result.push(personInfo);
  608. }
  609. }
  610. } else if (await personList.count() > 0) {
  611. // 如果是列表形式而非表格
  612. const listItems = personList.locator('[class*="item"], [class*="row"], li, div');
  613. const itemCount = await listItems.count();
  614. for (let i = 0; i < itemCount; i++) {
  615. const item = listItems.nth(i);
  616. const itemText = await item.textContent();
  617. if (itemText && itemText.trim()) {
  618. result.push({ name: itemText.trim() });
  619. }
  620. }
  621. }
  622. return result;
  623. }
  624. /**
  625. * 从订单详情对话框中获取附件列表
  626. * @returns 附件信息列表
  627. */
  628. async getAttachmentListFromDetail(): Promise<Array<{
  629. fileName?: string;
  630. uploadDate?: string;
  631. uploader?: string;
  632. }>> {
  633. const dialog = this.page.locator('[role="dialog"]');
  634. const result: Array<{ fileName?: string; uploadDate?: string; uploader?: string }> = [];
  635. // 查找附件列表区域
  636. // 尝试多种可能的定位策略
  637. const attachmentTable = dialog.locator('table').filter({ hasText: /附件|文件/ });
  638. const attachmentList = dialog.locator('[class*="attachment"], [class*="file"], [data-testid*="attachment"]');
  639. // 优先使用表格形式
  640. if (await attachmentTable.count() > 0) {
  641. const rows = attachmentTable.locator('tbody tr');
  642. const rowCount = await rows.count();
  643. for (let i = 0; i < rowCount; i++) {
  644. const row = rows.nth(i);
  645. const cells = row.locator('td');
  646. const cellCount = await cells.count();
  647. const attachmentInfo: { fileName?: string; uploadDate?: string; uploader?: string } = {};
  648. for (let j = 0; j < cellCount; j++) {
  649. const cellText = await cells.nth(j).textContent();
  650. if (!cellText) continue;
  651. const trimmedText = cellText.trim();
  652. // 文件名通常在第一列
  653. if (j === 0 && trimmedText) {
  654. attachmentInfo.fileName = trimmedText;
  655. }
  656. // 日期检查
  657. if (/^\d{4}-\d{2}-\d{2}/.test(trimmedText) || /^\d{4}\/\d{2}\/\d{2}/.test(trimmedText)) {
  658. if (!attachmentInfo.uploadDate) {
  659. attachmentInfo.uploadDate = trimmedText;
  660. }
  661. }
  662. // 上传者通常是文本用户名
  663. if (j > 0 && trimmedText && !attachmentInfo.uploader && !attachmentInfo.uploadDate && !/^\d{4}/.test(trimmedText)) {
  664. attachmentInfo.uploader = trimmedText;
  665. }
  666. }
  667. if (attachmentInfo.fileName) {
  668. result.push(attachmentInfo);
  669. }
  670. }
  671. } else if (await attachmentList.count() > 0) {
  672. // 如果是列表形式
  673. const listItems = attachmentList.locator('[class*="item"], [class*="row"], li, div');
  674. const itemCount = await listItems.count();
  675. for (let i = 0; i < itemCount; i++) {
  676. const item = listItems.nth(i);
  677. const itemText = await item.textContent();
  678. if (itemText && itemText.trim()) {
  679. result.push({ fileName: itemText.trim() });
  680. }
  681. }
  682. }
  683. return result;
  684. }
  685. /**
  686. * 关闭订单详情对话框
  687. */
  688. async closeDetailDialog(): Promise<void> {
  689. // 尝试多种关闭方式
  690. // 方式1: 点击右上角 X 按钮
  691. const closeButton = this.page.locator('[role="dialog"]').getByRole('button', { name: '关闭' }).first();
  692. const closeButtonCount = await closeButton.count();
  693. if (closeButtonCount > 0) {
  694. await closeButton.click();
  695. } else {
  696. // 方式2: 点击取消按钮
  697. const cancelButton = this.page.locator('[role="dialog"]').getByRole('button', { name: '取消' }).first();
  698. const cancelButtonCount = await cancelButton.count();
  699. if (cancelButtonCount > 0) {
  700. await cancelButton.click();
  701. } else {
  702. // 方式3: 按 Escape 键
  703. await this.page.keyboard.press('Escape');
  704. }
  705. }
  706. // 等待对话框关闭
  707. await this.waitForDialogClosed();
  708. }
  709. // ===== 人员关联管理 =====
  710. /**
  711. * 打开人员管理对话框
  712. *
  713. * **使用场景:**
  714. * - **从订单列表页打开**: 传入 `orderName` 参数,方法会先找到对应订单行,再点击人员管理按钮
  715. * - **从订单详情页打开**: 不传参数,方法会直接点击页面中的人员管理按钮
  716. *
  717. * @param orderName 订单名称(可选)。从列表页打开时需要传入,从详情页打开时不传
  718. *
  719. * @example
  720. * ```typescript
  721. * // 从订单列表页打开
  722. * await orderPage.openPersonManagementDialog('测试订单');
  723. *
  724. * // 从订单详情页打开
  725. * await orderPage.openDetailDialog('测试订单');
  726. * await orderPage.openPersonManagementDialog();
  727. * ```
  728. */
  729. async openPersonManagementDialog(orderName?: string) {
  730. // 如果提供了订单名称,先找到对应的订单行
  731. if (orderName) {
  732. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  733. const personButton = orderRow.getByRole('button', { name: /人员|员工/ });
  734. await personButton.click();
  735. } else {
  736. // 如果在详情页,直接点击人员管理按钮
  737. const personButton = this.page.getByRole('button', { name: /人员管理|添加人员/ });
  738. await personButton.click();
  739. }
  740. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  741. }
  742. /**
  743. * 添加人员到订单
  744. * @param personData 人员数据
  745. */
  746. async addPersonToOrder(personData: OrderPersonData) {
  747. // 点击添加人员按钮
  748. const addButton = this.page.getByRole('button', { name: /添加人员|新增人员/ });
  749. await addButton.click();
  750. await this.page.waitForTimeout(300);
  751. // 选择残疾人(支持通过名称选择)
  752. if (personData.disabledPersonName) {
  753. await selectRadixOption(this.page, '残疾人|选择残疾人', personData.disabledPersonName);
  754. } else if (personData.disabledPersonId) {
  755. // 如果只提供了 ID,尝试在对话框中选择第一个残疾人
  756. const firstCheckbox = this.page.locator('[role="dialog"]').locator('table tbody tr').first().locator('input[type="checkbox"]').first();
  757. try {
  758. await firstCheckbox.waitFor({ state: 'visible', timeout: 3000 });
  759. await firstCheckbox.check();
  760. } catch {
  761. console.debug('没有可用的残疾人数据');
  762. }
  763. }
  764. // 填写入职日期
  765. if (personData.hireDate) {
  766. const hireDateInput = this.page.getByLabel(/入职日期/);
  767. await hireDateInput.fill(personData.hireDate);
  768. }
  769. // 填写薪资
  770. if (personData.salary !== undefined) {
  771. const salaryInput = this.page.getByLabel(/薪资|工资/);
  772. await salaryInput.fill(String(personData.salary));
  773. }
  774. // 选择工作状态
  775. if (personData.workStatus) {
  776. const workStatusLabel = WORK_STATUS_LABELS[personData.workStatus];
  777. await selectRadixOption(this.page, '工作状态', workStatusLabel);
  778. }
  779. // 提交
  780. const submitButton = this.page.getByRole('button', { name: /^(添加|确定|保存)$/ });
  781. await submitButton.click();
  782. await this.page.waitForLoadState('networkidle');
  783. await this.page.waitForTimeout(1000);
  784. }
  785. /**
  786. * 修改人员工作状态
  787. * @param personName 人员姓名
  788. * @param newStatus 新的工作状态
  789. */
  790. async updatePersonWorkStatus(personName: string, newStatus: WorkStatus) {
  791. // 找到人员行
  792. const personRow = this.page.locator('[role="dialog"]').locator('table tbody tr').filter({ hasText: personName });
  793. // 点击编辑工作状态按钮
  794. const editButton = personRow.getByRole('button', { name: /编辑|修改/ });
  795. await editButton.click();
  796. await this.page.waitForTimeout(300);
  797. // 选择新的工作状态
  798. const workStatusLabel = WORK_STATUS_LABELS[newStatus];
  799. await selectRadixOption(this.page, '工作状态', workStatusLabel);
  800. // 提交
  801. const submitButton = this.page.getByRole('button', { name: /^(更新|保存|确定)$/ });
  802. await submitButton.click();
  803. await this.page.waitForLoadState('networkidle');
  804. await this.page.waitForTimeout(1000);
  805. }
  806. // ===== 附件管理 =====
  807. /**
  808. * 打开添加附件对话框
  809. */
  810. async openAddAttachmentDialog() {
  811. const attachmentButton = this.page.getByRole('button', { name: /添加附件|上传附件/ });
  812. await attachmentButton.click();
  813. await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
  814. }
  815. /**
  816. * 上传附件
  817. * @param personName 人员姓名
  818. * @param fileName 文件名
  819. * @param mimeType 文件类型(默认为 image/jpeg)
  820. */
  821. async uploadAttachment(personName: string, fileName: string, mimeType: string = 'image/jpeg') {
  822. // 选择订单人员
  823. const personSelect = this.page.getByLabel(/选择人员|订单人员/);
  824. await personSelect.click();
  825. await this.page.getByRole('option', { name: personName }).click();
  826. // 查找文件上传输入框
  827. const fileInput = this.page.locator('input[type="file"]');
  828. await fileInput.setInputFiles({
  829. name: fileName,
  830. mimeType,
  831. buffer: Buffer.from(`fake ${fileName} content`),
  832. });
  833. // 等待上传处理
  834. await this.page.waitForTimeout(500);
  835. // 提交
  836. const submitButton = this.page.getByRole('button', { name: /^(上传|确定|保存)$/ });
  837. await submitButton.click();
  838. await this.page.waitForLoadState('networkidle');
  839. await this.page.waitForTimeout(1000);
  840. }
  841. // ===== 高级操作 =====
  842. /**
  843. * 创建订单(完整流程)
  844. * @param data 订单数据
  845. * @returns 表单提交结果
  846. */
  847. async createOrder(data: OrderData): Promise<FormSubmitResult> {
  848. await this.openCreateDialog();
  849. await this.fillOrderForm(data);
  850. const result = await this.submitForm();
  851. await this.waitForDialogClosed();
  852. return result;
  853. }
  854. /**
  855. * 编辑订单(完整流程)
  856. * @param orderName 订单名称
  857. * @param data 更新的订单数据
  858. * @returns 表单提交结果
  859. */
  860. async editOrder(orderName: string, data: OrderData): Promise<FormSubmitResult> {
  861. await this.openEditDialog(orderName);
  862. await this.fillOrderForm(data);
  863. const result = await this.submitForm();
  864. await this.waitForDialogClosed();
  865. return result;
  866. }
  867. /**
  868. * 删除订单(完整流程)
  869. * @param orderName 订单名称
  870. * @returns 是否成功删除
  871. */
  872. async deleteOrder(orderName: string): Promise<boolean> {
  873. await this.openDeleteDialog(orderName);
  874. await this.confirmDelete();
  875. // 等待并检查 Toast 消息
  876. await this.page.waitForTimeout(1000);
  877. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  878. const hasSuccess = await successToast.count() > 0;
  879. return hasSuccess;
  880. }
  881. // ===== 订单状态流转操作 =====
  882. /**
  883. * 打开激活订单确认对话框
  884. * @param orderName 订单名称
  885. */
  886. async openActivateDialog(orderName: string): Promise<void> {
  887. // 找到订单行并点击"打开菜单"按钮(与编辑/删除操作相同的模式)
  888. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  889. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  890. await menuButton.click();
  891. // 等待菜单出现并点击"激活"选项
  892. const activateOption = this.page.getByRole('menuitem', { name: /激活|激活订单/ });
  893. await activateOption.waitFor({ state: 'visible', timeout: 3000 });
  894. await activateOption.click();
  895. // 等待确认对话框出现
  896. await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
  897. }
  898. /**
  899. * 确认激活订单
  900. */
  901. async confirmActivate(): Promise<void> {
  902. // 尝试多种可能的按钮名称
  903. const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', {
  904. name: /^(确认激活|激活|确定|确认)$/
  905. });
  906. await confirmButton.click();
  907. // 等待确认对话框关闭和网络请求完成
  908. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  909. .catch(() => console.debug('激活确认对话框关闭超时'));
  910. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  911. await this.page.waitForTimeout(1000);
  912. }
  913. /**
  914. * 激活订单(完整流程)
  915. * @param orderName 订单名称
  916. * @returns 是否成功激活
  917. */
  918. async activateOrder(orderName: string): Promise<boolean> {
  919. await this.openActivateDialog(orderName);
  920. await this.confirmActivate();
  921. // 等待并检查 Toast 消息
  922. await this.page.waitForTimeout(1000);
  923. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  924. const hasSuccess = await successToast.count() > 0;
  925. return hasSuccess;
  926. }
  927. /**
  928. * 打开关闭订单确认对话框
  929. * @param orderName 订单名称
  930. */
  931. async openCloseDialog(orderName: string): Promise<void> {
  932. // 找到订单行并点击"打开菜单"按钮
  933. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  934. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  935. await menuButton.click();
  936. // 等待菜单出现并点击"关闭"选项
  937. const closeOption = this.page.getByRole('menuitem', { name: /关闭|关闭订单|完成/ });
  938. await closeOption.waitFor({ state: 'visible', timeout: 3000 });
  939. await closeOption.click();
  940. // 等待确认对话框出现
  941. await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
  942. }
  943. /**
  944. * 确认关闭订单
  945. */
  946. async confirmClose(): Promise<void> {
  947. // 尝试多种可能的按钮名称
  948. const confirmButton = this.page.locator('[role="alertdialog"]').getByRole('button', {
  949. name: /^(确认关闭|关闭|确定|确认)$/
  950. });
  951. await confirmButton.click();
  952. // 等待确认对话框关闭和网络请求完成
  953. await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 })
  954. .catch(() => console.debug('关闭确认对话框关闭超时'));
  955. await this.page.waitForLoadState('networkidle', { timeout: 10000 });
  956. await this.page.waitForTimeout(1000);
  957. }
  958. /**
  959. * 关闭订单(完整流程)
  960. * @param orderName 订单名称
  961. * @returns 是否成功关闭
  962. */
  963. async closeOrder(orderName: string): Promise<boolean> {
  964. await this.openCloseDialog(orderName);
  965. await this.confirmClose();
  966. // 等待并检查 Toast 消息
  967. await this.page.waitForTimeout(1000);
  968. const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
  969. const hasSuccess = await successToast.count() > 0;
  970. return hasSuccess;
  971. }
  972. /**
  973. * 获取订单的当前状态(从列表页面)
  974. * @param orderName 订单名称
  975. * @returns 订单状态值或 null
  976. */
  977. async getOrderStatus(orderName: string): Promise<OrderStatus | null> {
  978. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  979. // 等待行可见
  980. await orderRow.waitFor({ state: 'visible', timeout: 3000 }).catch(() => {
  981. console.debug(`订单 "${orderName}" 行不可见`);
  982. });
  983. const rowCount = await orderRow.count();
  984. if (rowCount === 0) {
  985. console.debug(`订单 "${orderName}" 不存在`);
  986. return null;
  987. }
  988. // 尝试多种策略定位状态列
  989. // 策略1: 查找包含状态文本的单元格(但排除订单名称列)
  990. const allCells = orderRow.locator('td');
  991. const cellCount = await allCells.count();
  992. for (let i = 1; i < cellCount; i++) { // 跳过第一列(通常是订单名称)
  993. const cell = allCells.nth(i);
  994. const cellText = await cell.textContent();
  995. if (cellText) {
  996. // 检查是否包含完整的状态标签(避免部分匹配)
  997. for (const [statusValue, statusLabel] of Object.entries(ORDER_STATUS_LABELS)) {
  998. // 使用更严格的匹配:必须是状态标签本身或包含完整标签
  999. const trimmedText = cellText.trim();
  1000. if (trimmedText === statusLabel || trimmedText.includes(`${statusLabel}`)) {
  1001. // 验证不是订单名称列(额外检查)
  1002. const firstCellText = await allCells.nth(0).textContent();
  1003. if (firstCellText && !firstCellText.includes(orderName.substring(0, 3))) {
  1004. // 第一列不包含订单名称开头,说明列结构可能不同
  1005. return statusValue as OrderStatus;
  1006. }
  1007. // 跳过第一列后找到的状态标签才返回
  1008. return statusValue as OrderStatus;
  1009. }
  1010. }
  1011. }
  1012. }
  1013. // 策略2: 如果上述方法失败,尝试查找状态徽章/标签元素
  1014. // 查找具有状态样式特征的元素
  1015. const statusBadge = orderRow.locator('[class*="status"], [class*="badge"], span').filter({
  1016. hasText: Object.values(ORDER_STATUS_LABELS)
  1017. });
  1018. if (await statusBadge.count() > 0) {
  1019. const badgeText = await statusBadge.first().textContent();
  1020. if (badgeText) {
  1021. for (const [statusValue, statusLabel] of Object.entries(ORDER_STATUS_LABELS)) {
  1022. if (badgeText.includes(statusLabel)) {
  1023. return statusValue as OrderStatus;
  1024. }
  1025. }
  1026. }
  1027. }
  1028. console.debug(`无法从订单 "${orderName}" 中解析状态`);
  1029. return null;
  1030. }
  1031. /**
  1032. * 验证订单状态
  1033. * @param orderName 订单名称
  1034. * @param expectedStatus 期望的状态
  1035. */
  1036. async expectOrderStatus(orderName: string, expectedStatus: OrderStatus): Promise<void> {
  1037. const actualStatus = await this.getOrderStatus(orderName);
  1038. if (actualStatus === null) {
  1039. throw new Error(`订单 "${orderName}" 未找到或状态列无法识别`);
  1040. }
  1041. if (actualStatus !== expectedStatus) {
  1042. throw new Error(
  1043. `订单 "${orderName}" 状态不匹配: 期望 "${ORDER_STATUS_LABELS[expectedStatus]}", 实际 "${ORDER_STATUS_LABELS[actualStatus]}"`
  1044. );
  1045. }
  1046. }
  1047. /**
  1048. * 检查激活按钮是否可用
  1049. *
  1050. * **注意**: 此方法会打开和关闭菜单,属于有副作用的操作
  1051. *
  1052. * @param orderName 订单名称
  1053. * @returns 按钮是否可用
  1054. */
  1055. async checkActivateButtonEnabled(orderName: string): Promise<boolean> {
  1056. // 找到订单行并打开菜单
  1057. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  1058. // 检查订单是否存在
  1059. const orderCount = await orderRow.count();
  1060. if (orderCount === 0) {
  1061. console.debug(`订单 "${orderName}" 不存在`);
  1062. return false;
  1063. }
  1064. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  1065. try {
  1066. await menuButton.click();
  1067. } catch (error) {
  1068. console.debug(`无法打开订单 "${orderName}" 的菜单:`, error);
  1069. return false;
  1070. }
  1071. // 检查激活菜单项是否可点击
  1072. const activateOption = this.page.getByRole('menuitem', { name: /激活|激活订单/ });
  1073. const isVisible = await activateOption.isVisible().catch(() => false);
  1074. let isEnabled = false;
  1075. if (isVisible) {
  1076. // 检查是否有禁用属性或样式
  1077. const isDisabled = await activateOption.isDisabled().catch(() => false);
  1078. isEnabled = !isDisabled;
  1079. }
  1080. // 关闭菜单以便后续操作
  1081. await this.page.keyboard.press('Escape');
  1082. await this.page.waitForTimeout(300);
  1083. return isEnabled;
  1084. }
  1085. /**
  1086. * 检查关闭按钮是否可用
  1087. *
  1088. * **注意**: 此方法会打开和关闭菜单,属于有副作用的操作
  1089. *
  1090. * @param orderName 订单名称
  1091. * @returns 按钮是否可用
  1092. */
  1093. async checkCloseButtonEnabled(orderName: string): Promise<boolean> {
  1094. // 找到订单行并打开菜单
  1095. const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
  1096. // 检查订单是否存在
  1097. const orderCount = await orderRow.count();
  1098. if (orderCount === 0) {
  1099. console.debug(`订单 "${orderName}" 不存在`);
  1100. return false;
  1101. }
  1102. const menuButton = orderRow.getByRole('button', { name: '打开菜单' });
  1103. try {
  1104. await menuButton.click();
  1105. } catch (error) {
  1106. console.debug(`无法打开订单 "${orderName}" 的菜单:`, error);
  1107. return false;
  1108. }
  1109. // 检查关闭菜单项是否可点击
  1110. const closeOption = this.page.getByRole('menuitem', { name: /关闭|关闭订单|完成/ });
  1111. const isVisible = await closeOption.isVisible().catch(() => false);
  1112. let isEnabled = false;
  1113. if (isVisible) {
  1114. // 检查是否有禁用属性或样式
  1115. const isDisabled = await closeOption.isDisabled().catch(() => false);
  1116. isEnabled = !isDisabled;
  1117. }
  1118. // 关闭菜单以便后续操作
  1119. await this.page.keyboard.press('Escape');
  1120. await this.page.waitForTimeout(300);
  1121. return isEnabled;
  1122. }
  1123. }