import { expect, type Page, type Locator } from '@playwright/test'; import { Customer } from '@/utils/customer'; /** * 预约状态颜色 */ type colorStatus = { name: string; color: string; quantity: number; }; /** * 预约窗口操作 */ export enum AppointmentOperation { CANCEL = '取消预约', ADD_OCCUPY = '新建占用', ADD_APPOINT = '新建预约', ADD_REMARK = '添加备注', BACK = '返回', CANCEL_OCCUPY = '取消占用', } export class AppointmentPage { page: Page; $$name: Locator; $$time: Locator; $customer: Locator; $occupy: Locator; appointmentStatus: { NORMAL: colorStatus; CONSULT: colorStatus; BILL: colorStatus; SETTLED: colorStatus; EXPIRED: colorStatus; }; appointmentOperation: typeof AppointmentOperation; /** * 预约模块 * @param page 页面 */ constructor(page: Page) { this.page = page; this.$$name = this.page.locator('.header_table tr'); this.$$time = this.page.locator('.left_table td'); this.$customer = page.locator('.a_userInfo .user_name'); this.$occupy = page.locator('.a_userInfo .occupy'); this.appointmentStatus = { NORMAL: { name: '未到店', color: '', quantity: 0 }, CONSULT: { name: '已咨询', color: '', quantity: 0 }, BILL: { name: '已开单', color: '', quantity: 0 }, SETTLED: { name: '已结算', color: '', quantity: 0 }, EXPIRED: { name: '已过期', color: '', quantity: 0 }, }; } /** * 初始化预约页面 */ init = async () => { await this.page.getByRole('button', { name: /设\s置/ }).click(); await this.page.getByRole('menuitem', { name: '看板设置' }).click(); const $$btn = this.page.locator('.btnBox').getByRole('switch'); const $$btnCount = await $$btn.count(); // 遍历每个开关并将其状态设置为“开” for (let i = 0; i < $$btnCount; i++) { // 获取当前的开关按钮 const btn = $$btn.nth(i); await expect(async () => { if ((await btn.innerText()).trim() === '关') { await btn.click(); } await expect(btn).toContainText('开', { timeout: 3_000 }); }).toPass({ timeout: 30_000 }); } // 初始化未到店颜色 await this.page .locator('li', { hasText: /^未到店$/ }) .locator('svg') .click(); await this.page.getByLabel('Color:#D0021B').click(); await this.page.locator('div', { hasText: /^看板设置$/ }).click(); await this.page.locator('.close > svg').first().click(); await this.page.reload(); }; /** * 获取预约时间 * - 8:00 --> 8:30 * - 8:30 --> 9:00 * - 8:50 --> 9:00 */ getAppointmentTimesAvailable = (data = new Date()) => { if (!(data instanceof Date)) { throw new Error(`传入的${data}不是时间类型`); } const currentHour = data.getHours(); const nextTime = data.getMinutes() > 28 ? ':00' : ':30'; const hour = String(currentHour + (nextTime === ':00' ? 1 : 0)).padStart(2, '0'); return `${hour}${nextTime}`; }; /** * 获取预约状态 */ getAppointmentStatusSetting = async () => { await this.page.getByRole('button', { name: /设\s置/ }).waitFor(); await this.page.getByText('已过期').waitFor(); const statusArray = await this.page.locator('.herder_right ul > li').all(); for (const status of statusArray) { // 获取预约状态class name const statusClass = await status.locator('.status').getAttribute('class'); let name = statusClass ? statusClass.split(' ')[1] : ''; // 获取预约状态Color const color = await status.locator('.status').evaluate(e => { return window.getComputedStyle(e).backgroundColor; }); // 获取预约数量 const quantity = await status.locator('.number_type').innerText(); if (!name || !this.appointmentStatus[name]) { throw new Error(`没有获取${name}预约状态`); } const appointment = this.appointmentStatus[name]; appointment.color = color; appointment.quantity = quantity; } return this.appointmentStatus; }; /** * 获取顾客预约状态 * @param {Customer} customer 顾客 */ getCustomerAppointmentStatus = async (customer: Customer) => { const customerLocator = this.page.locator('.a_userInfo', { hasText: customer.username }).first(); // 点击顾客信息并获取状态 await customerLocator.locator('.user_name_info').click(); const customerStatus = await this.page.locator('.userInfo .state').innerText(); await this.page.locator('.close > svg > use').first().click(); // 获取顾客预约颜色 const customerColor = await customerLocator.locator('.appointment').evaluate(e => { return window.getComputedStyle(e).backgroundColor; }); return { customerColor, customerStatus }; }; /** * 根据员工和时间的中心进行点击预约单元格 */ openAppointmentCell = async (name: string, time?: Date, retry = 10) => { const currentAppointment = time ? this.getAppointmentTimesAvailable(time) : this.getAppointmentTimesAvailable(); const $currentTime = this.$$time.filter({ hasText: currentAppointment }); const currentTimeBoundingBox = await $currentTime.boundingBox(); if (name === '未指定') { await expect(async () => { if (await this.page.locator('.showNoAppoint_left').isVisible()) { await this.page.locator('.showNoAppoint_left').click(); } await expect(this.page.locator('.showNoAppoint_right')).toBeVisible({ timeout: 1000 }); }).toPass(); } const $name = this.$$name.filter({ hasText: name }).last(); const nameBoundingBox = await $name.boundingBox(); if (!currentTimeBoundingBox || !nameBoundingBox) { throw new Error( `Could not find bounding boxes: currentTimeBoundingBox=${currentTimeBoundingBox}, nameBoundingBox=${nameBoundingBox}`, ); } let widthCenter = nameBoundingBox.x + nameBoundingBox.width / 2; // 员工横坐标中心 let heightCenter = currentTimeBoundingBox.y + currentTimeBoundingBox.height / 2; // 时间纵坐标中心 const distance = currentTimeBoundingBox.height / 2; // 纵坐标位移一次的距离 /** * 获取元素的中心位置 */ const getElementPoint = async (x: number, y: number) => { return this.page.evaluate(({ x, y }) => document.elementFromPoint(x, y)?.outerHTML, { x, y }); }; for (let attempt = 0; attempt < retry; attempt++) { const elementPoint = await getElementPoint(widthCenter, heightCenter); // 根据是否有 user 单元格进行占用单元格 if (elementPoint?.includes('user') || elementPoint?.includes('occupy')) { heightCenter += distance; } else { // 点击预约单元格,弹出操作窗口,此处进行了重试 await expect(async () => { await this.page.mouse.click(widthCenter, heightCenter, { delay: 2000 }); await expect(this.page.locator('.popup_content', { hasText: '选择操作' })).toBeVisible({ timeout: 2000, }); }).toPass(); return; } } throw new Error(`Unable to click on the appointment cell after ${retry} attempts.`); }; /** * 打开预约详情 */ openAppointmentDetail = async (text: string) => { await this.page.locator('.a_userInfo', { hasText: text }).first().locator('.user_name_info').click(); await this.page.getByText('预约详情').waitFor(); }; /** * 关闭预约详情 */ closeAppointmentDetail = async () => { await this.page.locator('.close > svg > use').first().click(); }; /** * 取消预约 */ cancelAppoint = async () => { await this.page.locator('.state').waitFor(); const appointmentState = (await this.page.locator('.state').innerText()).trim(); let appointmentTimeStr: string; let appointmentTime: string; let hours: any; let minutes: any; let date: number | Date; let currentDate: number | Date; switch (appointmentState) { case '未到店': appointmentTimeStr = await this.page.locator('.time').innerText(); appointmentTime = appointmentTimeStr.split('-')[0]; [hours, minutes] = appointmentTime.split(':').map(Number); date = new Date(); date.setHours(hours, minutes, 0, 0); // 设置时、分、秒、毫秒 // 获取当前时间 currentDate = new Date(); // 比较时间 if (date >= currentDate) { // 当前时间小于或等于指定时间,执行操作 await this.page.getByRole('button', { name: '取消预约' }).click(); await this.page.getByRole('button', { name: /确\s认/ }).click(); await expect(this.page.locator('.ant-message', { hasText: '取消预约成功!' })).toBeVisible(); } else { await this.page.locator('.close > svg').first().click(); } break; case '已结算': await this.page.locator('.close > svg').first().click(); break; case '已开单': await this.page.locator('.close > svg').first().click(); break; case '已过期': await this.page.locator('.close > svg').first().click(); break; default: break; } }; /** * 判断元素的中心位置是否在可见视窗内 */ elementCenterInViewport = async (element: Locator) => { const box = await element.boundingBox(); const viewport = this.page.viewportSize(); if (!box) { return false; } const centerX = box.x + box.width / 2; const centerY = box.y + box.height / 2; return centerX >= 0 && centerX <= viewport!.width && centerY >= 0 && centerY <= viewport!.height; }; /** * 操作预约 * @param operation 操作 * - CANCEL 取消预约 * - ADD_OCCUPY 新建占用 * - ADD_APPOINT 新建预约 * - ADD_REMARK 添加备注 * - BACK 返回 * - CANCEL_OCCUPY 取消占用 */ operationAppointment = async (operation: AppointmentOperation) => { // 预约操作窗口 const $popup = this.page.locator('.popup_content .content'); // 点击操作 await expect(async () => { await $popup.getByText(operation).click(); await expect($popup.getByText(operation)).not.toBeInViewport({ timeout: 3000 }); }).toPass(); }; }