|
@@ -0,0 +1,726 @@
|
|
|
|
|
+import { Page, Locator } from '@playwright/test';
|
|
|
|
|
+import { selectRadixOption, selectRadixOptionAsync } from '@d8d/e2e-test-utils';
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 订单状态常量
|
|
|
|
|
+ */
|
|
|
|
|
+export const ORDER_STATUS = {
|
|
|
|
|
+ DRAFT: 'draft',
|
|
|
|
|
+ CONFIRMED: 'confirmed',
|
|
|
|
|
+ IN_PROGRESS: 'in_progress',
|
|
|
|
|
+ COMPLETED: 'completed',
|
|
|
|
|
+} as const;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 订单状态类型
|
|
|
|
|
+ */
|
|
|
|
|
+export type OrderStatus = typeof ORDER_STATUS[keyof typeof ORDER_STATUS];
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 订单状态显示名称映射
|
|
|
|
|
+ */
|
|
|
|
|
+export const ORDER_STATUS_LABELS: Record<OrderStatus, string> = {
|
|
|
|
|
+ draft: '草稿',
|
|
|
|
|
+ confirmed: '已确认',
|
|
|
|
|
+ in_progress: '进行中',
|
|
|
|
|
+ completed: '已完成',
|
|
|
|
|
+} as const;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 工作状态常量
|
|
|
|
|
+ */
|
|
|
|
|
+export const WORK_STATUS = {
|
|
|
|
|
+ NOT_EMPLOYED: 'not_employed',
|
|
|
|
|
+ PENDING: 'pending',
|
|
|
|
|
+ EMPLOYED: 'employed',
|
|
|
|
|
+ RESIGNED: 'resigned',
|
|
|
|
|
+} as const;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 工作状态类型
|
|
|
|
|
+ */
|
|
|
|
|
+export type WorkStatus = typeof WORK_STATUS[keyof typeof WORK_STATUS];
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 工作状态显示名称映射
|
|
|
|
|
+ */
|
|
|
|
|
+export const WORK_STATUS_LABELS: Record<WorkStatus, string> = {
|
|
|
|
|
+ not_employed: '未就业',
|
|
|
|
|
+ pending: '待就业',
|
|
|
|
|
+ employed: '已就业',
|
|
|
|
|
+ resigned: '已离职',
|
|
|
|
|
+} as const;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 订单数据接口
|
|
|
|
|
+ */
|
|
|
|
|
+export interface OrderData {
|
|
|
|
|
+ /** 订单名称 */
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ /** 预计开始日期 */
|
|
|
|
|
+ expectedStartDate?: string;
|
|
|
|
|
+ /** 平台ID */
|
|
|
|
|
+ platformId?: number;
|
|
|
|
|
+ /** 平台名称 */
|
|
|
|
|
+ platformName?: string;
|
|
|
|
|
+ /** 公司ID */
|
|
|
|
|
+ companyId?: number;
|
|
|
|
|
+ /** 公司名称 */
|
|
|
|
|
+ companyName?: string;
|
|
|
|
|
+ /** 渠道ID */
|
|
|
|
|
+ channelId?: number;
|
|
|
|
|
+ /** 渠道名称 */
|
|
|
|
|
+ channelName?: string;
|
|
|
|
|
+ /** 订单状态 */
|
|
|
|
|
+ status?: OrderStatus;
|
|
|
|
|
+ /** 工作状态 */
|
|
|
|
|
+ workStatus?: WorkStatus;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 订单人员数据接口
|
|
|
|
|
+ */
|
|
|
|
|
+export interface OrderPersonData {
|
|
|
|
|
+ /** 残疾人ID */
|
|
|
|
|
+ disabledPersonId: number;
|
|
|
|
|
+ /** 残疾人姓名 */
|
|
|
|
|
+ disabledPersonName?: string;
|
|
|
|
|
+ /** 入职日期 */
|
|
|
|
|
+ hireDate?: string;
|
|
|
|
|
+ /** 薪资 */
|
|
|
|
|
+ salary?: number;
|
|
|
|
|
+ /** 工作状态 */
|
|
|
|
|
+ workStatus?: WorkStatus;
|
|
|
|
|
+ /** 实际入职日期 */
|
|
|
|
|
+ actualHireDate?: string;
|
|
|
|
|
+ /** 离职日期 */
|
|
|
|
|
+ resignDate?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 网络响应数据接口
|
|
|
|
|
+ */
|
|
|
|
|
+export interface NetworkResponse {
|
|
|
|
|
+ /** 请求URL */
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ /** 请求方法 */
|
|
|
|
|
+ method: string;
|
|
|
|
|
+ /** 响应状态码 */
|
|
|
|
|
+ status: number;
|
|
|
|
|
+ /** 是否成功 */
|
|
|
|
|
+ ok: boolean;
|
|
|
|
|
+ /** 响应头 */
|
|
|
|
|
+ responseHeaders: Record<string, string>;
|
|
|
|
|
+ /** 响应体 */
|
|
|
|
|
+ responseBody: unknown;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 表单提交结果接口
|
|
|
|
|
+ */
|
|
|
|
|
+export interface FormSubmitResult {
|
|
|
|
|
+ /** 提交是否成功 */
|
|
|
|
|
+ success: boolean;
|
|
|
|
|
+ /** 是否有错误 */
|
|
|
|
|
+ hasError: boolean;
|
|
|
|
|
+ /** 是否有成功消息 */
|
|
|
|
|
+ hasSuccess: boolean;
|
|
|
|
|
+ /** 错误消息 */
|
|
|
|
|
+ errorMessage?: string;
|
|
|
|
|
+ /** 成功消息 */
|
|
|
|
|
+ successMessage?: string;
|
|
|
|
|
+ /** 网络响应列表 */
|
|
|
|
|
+ responses?: NetworkResponse[];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 订单管理 Page Object
|
|
|
|
|
+ *
|
|
|
|
|
+ * 用于订单管理功能的 E2E 测试
|
|
|
|
|
+ * 页面路径: /admin/orders(待确认)
|
|
|
|
|
+ *
|
|
|
|
|
+ * @example
|
|
|
|
|
+ * ```typescript
|
|
|
|
|
+ * const orderPage = new OrderManagementPage(page);
|
|
|
|
|
+ * await orderPage.goto();
|
|
|
|
|
+ * await orderPage.createOrder({ name: '测试订单' });
|
|
|
|
|
+ * ```
|
|
|
|
|
+ */
|
|
|
|
|
+export class OrderManagementPage {
|
|
|
|
|
+ readonly page: Page;
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 页面级选择器 =====
|
|
|
|
|
+ /** 页面标题 */
|
|
|
|
|
+ readonly pageTitle: Locator;
|
|
|
|
|
+ /** 新增订单按钮 */
|
|
|
|
|
+ readonly addOrderButton: Locator;
|
|
|
|
|
+ /** 订单列表表格 */
|
|
|
|
|
+ readonly orderTable: Locator;
|
|
|
|
|
+ /** 搜索输入框 */
|
|
|
|
|
+ readonly searchInput: Locator;
|
|
|
|
|
+ /** 搜索按钮 */
|
|
|
|
|
+ readonly searchButton: Locator;
|
|
|
|
|
+
|
|
|
|
|
+ constructor(page: Page) {
|
|
|
|
|
+ this.page = page;
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化页面级选择器
|
|
|
|
|
+ this.pageTitle = page.getByText('订单管理', { exact: true });
|
|
|
|
|
+ this.addOrderButton = page.getByRole('button', { name: '新增订单', exact: true });
|
|
|
|
|
+ this.orderTable = page.locator('table');
|
|
|
|
|
+ this.searchInput = page.getByPlaceholder('搜索订单名称');
|
|
|
|
|
+ this.searchButton = page.getByRole('button', { name: '搜索' });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 导航和基础验证 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 导航到订单管理页面
|
|
|
|
|
+ */
|
|
|
|
|
+ async goto() {
|
|
|
|
|
+ await this.page.goto('/admin/orders');
|
|
|
|
|
+ await this.page.waitForLoadState('domcontentloaded');
|
|
|
|
|
+ // 等待页面标题出现
|
|
|
|
|
+ await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
|
|
|
|
|
+ // 等待表格数据加载
|
|
|
|
|
+ await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: 20000 });
|
|
|
|
|
+ await this.expectToBeVisible();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证页面关键元素可见
|
|
|
|
|
+ */
|
|
|
|
|
+ async expectToBeVisible() {
|
|
|
|
|
+ await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
|
|
|
|
|
+ await this.addOrderButton.waitFor({ state: 'visible', timeout: 10000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 搜索和筛选功能 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按订单名称搜索
|
|
|
|
|
+ * @param name 订单名称
|
|
|
|
|
+ */
|
|
|
|
|
+ async searchByName(name: string) {
|
|
|
|
|
+ await this.searchInput.fill(name);
|
|
|
|
|
+ await this.searchButton.click();
|
|
|
|
|
+ await this.page.waitForLoadState('networkidle');
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开高级筛选对话框
|
|
|
|
|
+ */
|
|
|
|
|
+ async openFilterDialog() {
|
|
|
|
|
+ const filterButton = this.page.getByRole('button', { name: /筛选|高级筛选/ });
|
|
|
|
|
+ await filterButton.click();
|
|
|
|
|
+ await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置筛选条件
|
|
|
|
|
+ * @param filters 筛选条件
|
|
|
|
|
+ */
|
|
|
|
|
+ async setFilters(filters: {
|
|
|
|
|
+ status?: OrderStatus;
|
|
|
|
|
+ workStatus?: WorkStatus;
|
|
|
|
|
+ platformId?: number;
|
|
|
|
|
+ platformName?: string;
|
|
|
|
|
+ companyId?: number;
|
|
|
|
|
+ companyName?: string;
|
|
|
|
|
+ channelId?: number;
|
|
|
|
|
+ channelName?: string;
|
|
|
|
|
+ dateRange?: { start?: string; end?: string };
|
|
|
|
|
+ }) {
|
|
|
|
|
+ // 订单状态筛选
|
|
|
|
|
+ if (filters.status || filters.workStatus) {
|
|
|
|
|
+ const statusFilter = this.page.getByLabel(/订单状态|状态/);
|
|
|
|
|
+ await statusFilter.click();
|
|
|
|
|
+ const statusLabel = filters.status
|
|
|
|
|
+ ? ORDER_STATUS_LABELS[filters.status]
|
|
|
|
|
+ : undefined;
|
|
|
|
|
+ if (statusLabel) {
|
|
|
|
|
+ await this.page.getByRole('option', { name: statusLabel }).click();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 平台筛选
|
|
|
|
|
+ if (filters.platformName) {
|
|
|
|
|
+ await selectRadixOption(this.page, '平台', filters.platformName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 公司筛选
|
|
|
|
|
+ if (filters.companyName) {
|
|
|
|
|
+ await selectRadixOption(this.page, '公司', filters.companyName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 渠道筛选
|
|
|
|
|
+ if (filters.channelName) {
|
|
|
|
|
+ await selectRadixOption(this.page, '渠道', filters.channelName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 日期范围筛选
|
|
|
|
|
+ if (filters.dateRange) {
|
|
|
|
|
+ if (filters.dateRange.start) {
|
|
|
|
|
+ const startDateInput = this.page.getByLabel(/开始日期|起始日期/);
|
|
|
|
|
+ await startDateInput.fill(filters.dateRange.start);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (filters.dateRange.end) {
|
|
|
|
|
+ const endDateInput = this.page.getByLabel(/结束日期|截止日期/);
|
|
|
|
|
+ await endDateInput.fill(filters.dateRange.end);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 应用筛选条件
|
|
|
|
|
+ */
|
|
|
|
|
+ async applyFilters() {
|
|
|
|
|
+ const applyButton = this.page.getByRole('button', { name: /应用|确定|筛选/ });
|
|
|
|
|
+ await applyButton.click();
|
|
|
|
|
+ await this.page.waitForLoadState('networkidle');
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 清空筛选条件
|
|
|
|
|
+ */
|
|
|
|
|
+ async clearFilters() {
|
|
|
|
|
+ const clearButton = this.page.getByRole('button', { name: /重置|清空/ });
|
|
|
|
|
+ await clearButton.click();
|
|
|
|
|
+ await this.page.waitForTimeout(500);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 订单 CRUD 操作 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开创建订单对话框
|
|
|
|
|
+ */
|
|
|
|
|
+ async openCreateDialog() {
|
|
|
|
|
+ await this.addOrderButton.click();
|
|
|
|
|
+ await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开编辑订单对话框
|
|
|
|
|
+ * @param orderName 订单名称
|
|
|
|
|
+ */
|
|
|
|
|
+ async openEditDialog(orderName: string) {
|
|
|
|
|
+ // 找到订单行并点击编辑按钮
|
|
|
|
|
+ const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
|
|
|
|
|
+ const editButton = orderRow.getByRole('button', { name: '编辑' });
|
|
|
|
|
+ await editButton.click();
|
|
|
|
|
+ await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开删除确认对话框
|
|
|
|
|
+ * @param orderName 订单名称
|
|
|
|
|
+ */
|
|
|
|
|
+ async openDeleteDialog(orderName: string) {
|
|
|
|
|
+ // 找到订单行并点击删除按钮
|
|
|
|
|
+ const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
|
|
|
|
|
+ const deleteButton = orderRow.getByRole('button', { name: '删除' });
|
|
|
|
|
+ await deleteButton.click();
|
|
|
|
|
+ await this.page.waitForSelector('[role="alertdialog"]', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 填写订单表单
|
|
|
|
|
+ * @param data 订单数据
|
|
|
|
|
+ */
|
|
|
|
|
+ async fillOrderForm(data: OrderData) {
|
|
|
|
|
+ // 等待表单出现
|
|
|
|
|
+ await this.page.waitForSelector('form', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+
|
|
|
|
|
+ // 填写订单名称
|
|
|
|
|
+ if (data.name) {
|
|
|
|
|
+ await this.page.getByLabel(/订单名称|名称/).fill(data.name);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 填写预计开始日期
|
|
|
|
|
+ if (data.expectedStartDate) {
|
|
|
|
|
+ const dateInput = this.page.getByLabel(/预计开始日期|开始日期/);
|
|
|
|
|
+ await dateInput.fill(data.expectedStartDate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 选择平台
|
|
|
|
|
+ if (data.platformName) {
|
|
|
|
|
+ await selectRadixOption(this.page, '平台', data.platformName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 选择公司
|
|
|
|
|
+ if (data.companyName) {
|
|
|
|
|
+ await selectRadixOption(this.page, '公司', data.companyName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 选择渠道
|
|
|
|
|
+ if (data.channelName) {
|
|
|
|
|
+ await selectRadixOption(this.page, '渠道', data.channelName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 选择订单状态(如果是编辑模式)
|
|
|
|
|
+ if (data.status) {
|
|
|
|
|
+ const statusLabel = ORDER_STATUS_LABELS[data.status];
|
|
|
|
|
+ await selectRadixOption(this.page, '订单状态', statusLabel);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 选择工作状态(如果是编辑模式)
|
|
|
|
|
+ if (data.workStatus) {
|
|
|
|
|
+ const workStatusLabel = WORK_STATUS_LABELS[data.workStatus];
|
|
|
|
|
+ await selectRadixOption(this.page, '工作状态', workStatusLabel);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 提交表单
|
|
|
|
|
+ * @returns 表单提交结果
|
|
|
|
|
+ */
|
|
|
|
|
+ async submitForm(): Promise<FormSubmitResult> {
|
|
|
|
|
+ // 收集网络响应
|
|
|
|
|
+ const responses: NetworkResponse[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 监听所有网络请求
|
|
|
|
|
+ const responseHandler = async (response: Response) => {
|
|
|
|
|
+ const url = response.url();
|
|
|
|
|
+ // 监听订单管理相关的 API 请求
|
|
|
|
|
+ if (url.includes('/orders') || url.includes('order')) {
|
|
|
|
|
+ const requestBody = response.request()?.postData();
|
|
|
|
|
+ const responseBody = await response.text().catch(() => '');
|
|
|
|
|
+ let jsonBody = null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ jsonBody = JSON.parse(responseBody);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 不是 JSON 响应
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ responses.push({
|
|
|
|
|
+ url,
|
|
|
|
|
+ method: response.request()?.method() ?? 'UNKNOWN',
|
|
|
|
|
+ status: response.status(),
|
|
|
|
|
+ ok: response.ok(),
|
|
|
|
|
+ responseHeaders: await response.allHeaders().catch(() => ({})),
|
|
|
|
|
+ responseBody: jsonBody || responseBody,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.page.on('response', responseHandler);
|
|
|
|
|
+
|
|
|
|
|
+ // 点击提交按钮(创建或更新)
|
|
|
|
|
+ const submitButton = this.page.getByRole('button', { name: /^(创建|更新|保存)$/ });
|
|
|
|
|
+ await submitButton.click();
|
|
|
|
|
+
|
|
|
|
|
+ // 等待网络请求完成
|
|
|
|
|
+ await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
|
|
|
|
+
|
|
|
|
|
+ // 移除监听器
|
|
|
|
|
+ this.page.off('response', responseHandler);
|
|
|
|
|
+
|
|
|
|
|
+ // 等待 Toast 消息显示
|
|
|
|
|
+ await this.page.waitForTimeout(2000);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查 Toast 消息
|
|
|
|
|
+ const errorToast = this.page.locator('[data-sonner-toast][data-type="error"]');
|
|
|
|
|
+ const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
|
|
|
|
|
+
|
|
|
|
|
+ const hasError = await errorToast.count() > 0;
|
|
|
|
|
+ const hasSuccess = await successToast.count() > 0;
|
|
|
|
|
+
|
|
|
|
|
+ let errorMessage: string | null = null;
|
|
|
|
|
+ let successMessage: string | null = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (hasError) {
|
|
|
|
|
+ errorMessage = await errorToast.first().textContent();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (hasSuccess) {
|
|
|
|
|
+ successMessage = await successToast.first().textContent();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: hasSuccess || (!hasError && !hasSuccess),
|
|
|
|
|
+ hasError,
|
|
|
|
|
+ hasSuccess,
|
|
|
|
|
+ errorMessage: errorMessage ?? undefined,
|
|
|
|
|
+ successMessage: successMessage ?? undefined,
|
|
|
|
|
+ responses,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取消对话框
|
|
|
|
|
+ */
|
|
|
|
|
+ async cancelDialog() {
|
|
|
|
|
+ const cancelButton = this.page.getByRole('button', { name: '取消' });
|
|
|
|
|
+ await cancelButton.click();
|
|
|
|
|
+ await this.waitForDialogClosed();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 等待对话框关闭
|
|
|
|
|
+ */
|
|
|
|
|
+ async waitForDialogClosed() {
|
|
|
|
|
+ const dialog = this.page.locator('[role="dialog"]');
|
|
|
|
|
+ await dialog.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
|
|
|
+ await this.page.waitForTimeout(500);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 确认删除操作
|
|
|
|
|
+ */
|
|
|
|
|
+ async confirmDelete() {
|
|
|
|
|
+ const confirmButton = this.page.getByRole('button', { name: /^确认删除$/ });
|
|
|
|
|
+ await confirmButton.click();
|
|
|
|
|
+ // 等待确认对话框关闭和网络请求完成
|
|
|
|
|
+ await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
|
|
|
+ await this.page.waitForLoadState('networkidle', { timeout: 10000 });
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 取消删除操作
|
|
|
|
|
+ */
|
|
|
|
|
+ async cancelDelete() {
|
|
|
|
|
+ const cancelButton = this.page.getByRole('button', { name: '取消' }).and(
|
|
|
|
|
+ this.page.locator('[role="alertdialog"]')
|
|
|
|
|
+ );
|
|
|
|
|
+ await cancelButton.click();
|
|
|
|
|
+ await this.page.waitForSelector('[role="alertdialog"]', { state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证订单是否存在
|
|
|
|
|
+ * @param orderName 订单名称
|
|
|
|
|
+ * @returns 订单是否存在
|
|
|
|
|
+ */
|
|
|
|
|
+ async orderExists(orderName: string): Promise<boolean> {
|
|
|
|
|
+ const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
|
|
|
|
|
+ return (await orderRow.count()) > 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 订单详情 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开订单详情对话框
|
|
|
|
|
+ * @param orderName 订单名称
|
|
|
|
|
+ */
|
|
|
|
|
+ async openDetailDialog(orderName: string) {
|
|
|
|
|
+ // 找到订单行并点击查看详情按钮
|
|
|
|
|
+ const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
|
|
|
|
|
+ const detailButton = orderRow.getByRole('button', { name: /详情|查看/ });
|
|
|
|
|
+ await detailButton.click();
|
|
|
|
|
+ await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取订单详情中的基本信息
|
|
|
|
|
+ * @returns 订单基本信息
|
|
|
|
|
+ */
|
|
|
|
|
+ async getOrderDetailInfo(): Promise<{
|
|
|
|
|
+ name?: string;
|
|
|
|
|
+ status?: string;
|
|
|
|
|
+ workStatus?: string;
|
|
|
|
|
+ expectedStartDate?: string;
|
|
|
|
|
+ platform?: string;
|
|
|
|
|
+ company?: string;
|
|
|
|
|
+ channel?: string;
|
|
|
|
|
+ }> {
|
|
|
|
|
+ const result: Record<string, string> = {};
|
|
|
|
|
+
|
|
|
|
|
+ // 订单名称
|
|
|
|
|
+ const nameElement = this.page.locator('[role="dialog"]').getByText(/订单名称/);
|
|
|
|
|
+ if (await nameElement.count() > 0) {
|
|
|
|
|
+ result.name = await nameElement.textContent();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 订单状态
|
|
|
|
|
+ const statusElement = this.page.locator('[role="dialog"]').getByText(/订单状态/);
|
|
|
|
|
+ if (await statusElement.count() > 0) {
|
|
|
|
|
+ result.status = await statusElement.textContent();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 工作状态
|
|
|
|
|
+ const workStatusElement = this.page.locator('[role="dialog"]').getByText(/工作状态/);
|
|
|
|
|
+ if (await workStatusElement.count() > 0) {
|
|
|
|
|
+ result.workStatus = await workStatusElement.textContent();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 人员关联管理 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开人员管理对话框
|
|
|
|
|
+ * @param orderName 订单名称(如果在订单列表页)
|
|
|
|
|
+ */
|
|
|
|
|
+ async openPersonManagementDialog(orderName?: string) {
|
|
|
|
|
+ // 如果提供了订单名称,先找到对应的订单行
|
|
|
|
|
+ if (orderName) {
|
|
|
|
|
+ const orderRow = this.orderTable.locator('tbody tr').filter({ hasText: orderName });
|
|
|
|
|
+ const personButton = orderRow.getByRole('button', { name: /人员|员工/ });
|
|
|
|
|
+ await personButton.click();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果在详情页,直接点击人员管理按钮
|
|
|
|
|
+ const personButton = this.page.getByRole('button', { name: /人员管理|添加人员/ });
|
|
|
|
|
+ await personButton.click();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 添加人员到订单
|
|
|
|
|
+ * @param personData 人员数据
|
|
|
|
|
+ */
|
|
|
|
|
+ async addPersonToOrder(personData: OrderPersonData) {
|
|
|
|
|
+ // 点击添加人员按钮
|
|
|
|
|
+ const addButton = this.page.getByRole('button', { name: /添加人员|新增人员/ });
|
|
|
|
|
+ await addButton.click();
|
|
|
|
|
+ await this.page.waitForTimeout(300);
|
|
|
|
|
+
|
|
|
|
|
+ // 选择残疾人
|
|
|
|
|
+ if (personData.disabledPersonName) {
|
|
|
|
|
+ await selectRadixOption(this.page, '残疾人|选择残疾人', personData.disabledPersonName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 填写入职日期
|
|
|
|
|
+ if (personData.hireDate) {
|
|
|
|
|
+ const hireDateInput = this.page.getByLabel(/入职日期/);
|
|
|
|
|
+ await hireDateInput.fill(personData.hireDate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 填写薪资
|
|
|
|
|
+ if (personData.salary !== undefined) {
|
|
|
|
|
+ const salaryInput = this.page.getByLabel(/薪资|工资/);
|
|
|
|
|
+ await salaryInput.fill(String(personData.salary));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 选择工作状态
|
|
|
|
|
+ if (personData.workStatus) {
|
|
|
|
|
+ const workStatusLabel = WORK_STATUS_LABELS[personData.workStatus];
|
|
|
|
|
+ await selectRadixOption(this.page, '工作状态', workStatusLabel);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 提交
|
|
|
|
|
+ const submitButton = this.page.getByRole('button', { name: /^(添加|确定|保存)$/ });
|
|
|
|
|
+ await submitButton.click();
|
|
|
|
|
+
|
|
|
|
|
+ await this.page.waitForLoadState('networkidle');
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 修改人员工作状态
|
|
|
|
|
+ * @param personName 人员姓名
|
|
|
|
|
+ * @param newStatus 新的工作状态
|
|
|
|
|
+ */
|
|
|
|
|
+ async updatePersonWorkStatus(personName: string, newStatus: WorkStatus) {
|
|
|
|
|
+ // 找到人员行
|
|
|
|
|
+ const personRow = this.page.locator('[role="dialog"]').locator('table tbody tr').filter({ hasText: personName });
|
|
|
|
|
+
|
|
|
|
|
+ // 点击编辑工作状态按钮
|
|
|
|
|
+ const editButton = personRow.getByRole('button', { name: /编辑|修改/ });
|
|
|
|
|
+ await editButton.click();
|
|
|
|
|
+ await this.page.waitForTimeout(300);
|
|
|
|
|
+
|
|
|
|
|
+ // 选择新的工作状态
|
|
|
|
|
+ const workStatusLabel = WORK_STATUS_LABELS[newStatus];
|
|
|
|
|
+ await selectRadixOption(this.page, '工作状态', workStatusLabel);
|
|
|
|
|
+
|
|
|
|
|
+ // 提交
|
|
|
|
|
+ const submitButton = this.page.getByRole('button', { name: /^(更新|保存|确定)$/ });
|
|
|
|
|
+ await submitButton.click();
|
|
|
|
|
+
|
|
|
|
|
+ await this.page.waitForLoadState('networkidle');
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 附件管理 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 打开添加附件对话框
|
|
|
|
|
+ */
|
|
|
|
|
+ async openAddAttachmentDialog() {
|
|
|
|
|
+ const attachmentButton = this.page.getByRole('button', { name: /添加附件|上传附件/ });
|
|
|
|
|
+ await attachmentButton.click();
|
|
|
|
|
+ await this.page.waitForSelector('[role="dialog"]', { state: 'visible', timeout: 5000 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传附件
|
|
|
|
|
+ * @param personName 人员姓名
|
|
|
|
|
+ * @param fileName 文件名
|
|
|
|
|
+ * @param mimeType 文件类型(默认为 image/jpeg)
|
|
|
|
|
+ */
|
|
|
|
|
+ async uploadAttachment(personName: string, fileName: string, mimeType: string = 'image/jpeg') {
|
|
|
|
|
+ // 选择订单人员
|
|
|
|
|
+ const personSelect = this.page.getByLabel(/选择人员|订单人员/);
|
|
|
|
|
+ await personSelect.click();
|
|
|
|
|
+ await this.page.getByRole('option', { name: personName }).click();
|
|
|
|
|
+
|
|
|
|
|
+ // 查找文件上传输入框
|
|
|
|
|
+ const fileInput = this.page.locator('input[type="file"]');
|
|
|
|
|
+ await fileInput.setInputFiles({
|
|
|
|
|
+ name: fileName,
|
|
|
|
|
+ mimeType,
|
|
|
|
|
+ buffer: Buffer.from(`fake ${fileName} content`),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 等待上传处理
|
|
|
|
|
+ await this.page.waitForTimeout(500);
|
|
|
|
|
+
|
|
|
|
|
+ // 提交
|
|
|
|
|
+ const submitButton = this.page.getByRole('button', { name: /^(上传|确定|保存)$/ });
|
|
|
|
|
+ await submitButton.click();
|
|
|
|
|
+
|
|
|
|
|
+ await this.page.waitForLoadState('networkidle');
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 高级操作 =====
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建订单(完整流程)
|
|
|
|
|
+ * @param data 订单数据
|
|
|
|
|
+ * @returns 表单提交结果
|
|
|
|
|
+ */
|
|
|
|
|
+ async createOrder(data: OrderData): Promise<FormSubmitResult> {
|
|
|
|
|
+ await this.openCreateDialog();
|
|
|
|
|
+ await this.fillOrderForm(data);
|
|
|
|
|
+ const result = await this.submitForm();
|
|
|
|
|
+ await this.waitForDialogClosed();
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 编辑订单(完整流程)
|
|
|
|
|
+ * @param orderName 订单名称
|
|
|
|
|
+ * @param data 更新的订单数据
|
|
|
|
|
+ * @returns 表单提交结果
|
|
|
|
|
+ */
|
|
|
|
|
+ async editOrder(orderName: string, data: OrderData): Promise<FormSubmitResult> {
|
|
|
|
|
+ await this.openEditDialog(orderName);
|
|
|
|
|
+ await this.fillOrderForm(data);
|
|
|
|
|
+ const result = await this.submitForm();
|
|
|
|
|
+ await this.waitForDialogClosed();
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除订单(完整流程)
|
|
|
|
|
+ * @param orderName 订单名称
|
|
|
|
|
+ * @returns 是否成功删除
|
|
|
|
|
+ */
|
|
|
|
|
+ async deleteOrder(orderName: string): Promise<boolean> {
|
|
|
|
|
+ await this.openDeleteDialog(orderName);
|
|
|
|
|
+ await this.confirmDelete();
|
|
|
|
|
+
|
|
|
|
|
+ // 等待并检查 Toast 消息
|
|
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
+ const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
|
|
|
|
|
+ const hasSuccess = await successToast.count() > 0;
|
|
|
|
|
+
|
|
|
|
|
+ return hasSuccess;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|