import { expect, type Locator, type Page } from '@playwright/test'; import { Customer, employee } from '@/utils/customer'; import { waitSpecifyApiLoad } from '@/utils/utils'; type SubPage = { name: string; url: string[]; }; /** * `CustomerPage` 类提供了与应用程序的顾客页面交互的方法。 * 它包括导航到子页面、搜索顾客、打开和关闭顾客详情、创建新顾客以及将顾客设置为无效的功能。 */ export class CustomerPage { page: Page; private readonly subPages: SubPage[] = [ { name: '顾客概要', url: ['summary', 'todo'] }, { name: '顾客分配', url: ['search_new', 'distribution'] }, { name: '顾客动态', url: ['daily_action'] }, { name: '顾客分析', url: ['analysis'] }, { name: '服务日志', url: ['service_log'] }, ]; private readonly firstStore = { firstDepartment: { no: 1, name: '美容部' }, secondDepartment: { no: 2, name: '医美部' }, }; private readonly secondStore = { firstDepartment: { no: 1, name: '美容部' }, }; private readonly sourceChannel: string[] = [ '邀客', '员工带客', '美团', '大众点评', '客带客', '上门客人', '百度糯米', '支付宝', ]; $register: Locator; $tabItem: Locator; $searchInput: Locator; $searchBtn: Locator; constructor(page: Page) { this.page = page; this.$tabItem = this.page.locator('.top_tab .tab_item'); this.$register = this.page.locator('.regmeber_warp', { hasText: '创建会员' }); this.$searchInput = this.page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索'); this.$searchBtn = this.page.getByText('搜索', { exact: true }); } /** * 跳转子页面,并且等待页面接口加载完成 * @param {string} subPageName 顾客模块子页面 * - 顾客概要 * - 顾客分配 * - 顾客动态 * - 顾客分析 * - 服务日志 */ gotoSubPage = async (subPageName: string) => { // 输入验证 if (!subPageName || typeof subPageName !== 'string' || subPageName.trim() === '') { throw new Error('子页面名称不能为空或无效'); } const subPage = this.subPages.find(e => e.name === subPageName); if (!subPage) { console.error(`子页面 ${subPageName} 不存在`); throw new Error(`子页面 ${subPageName} 不存在`); } const $subPageTab = this.$tabItem.filter({ hasText: subPageName }); await $subPageTab.waitFor(); try { const classAttribute = await $subPageTab.getAttribute('class', { timeout: 5000 }); if (classAttribute && classAttribute.includes('active')) { return; } } catch (error) { console.error(`获取子页面 ${subPageName} 的 class 属性超时`); throw new Error(`获取子页面 ${subPageName} 的 class 属性超时`); } try { await $subPageTab.click(); await expect($subPageTab).toHaveClass(/active/, { timeout: 5000 }); await waitSpecifyApiLoad(this.page, subPage.url); } catch (error) { console.error(`点击子页面 ${subPageName} 或等待 API 加载失败`); throw new Error(`点击子页面 ${subPageName} 或等待 API 加载失败`); } }; /** * 搜索顾客 * @param text 姓名(拼音首字)、手机号、档案号搜索 */ searchCustomer = async (text: string) => { const $$customerContent = this.page.locator('.member_list_li .custom_content'); const $customerInfoCard = $$customerContent.filter({ hasText: text }).first(); await this.$searchInput.fill(text); await this.$searchBtn.click(); await $customerInfoCard.waitFor(); }; /** * 选择搜索顾客 * @param text 姓名(拼音首字)、手机号、档案号搜索 */ selectSearchCustomer = async (text: string) => { const $$customerContent = this.page.locator('.member_list_li .custom_content'); const $customerInfoCard = $$customerContent.filter({ hasText: text }).first(); await $customerInfoCard.click(); await $customerInfoCard.waitFor({ state: 'detached' }); }; /** * 打开顾客详情页面 * @param {string} username * @param {string} phone */ openCustomerDetail = async (username: string, phone: string) => { const $customer = this.page .locator('.m-table__fixed-left td') .filter({ hasText: username }) .filter({ hasText: phone }) .first(); const $username = this.page.getByRole('tabpanel').getByText(username); const $phone = this.page.getByRole('tabpanel').getByText(phone); await Promise.all([ await $customer.click(), this.page.waitForResponse(res => res.url().includes('/api/customer') && res.status() === 200), ]); await $username.waitFor(); await $phone.waitFor(); }; /** * 关闭顾客详情页面 */ closeCustomerDetail = async () => { const $closeBtn = this.page.locator('.member_info_box .close_icons > svg'); await $closeBtn.click(); await $closeBtn.waitFor({ state: 'detached' }); }; /** * 创建顾客 */ createCustomer = async (customer: Customer) => { const customerPage = '/#/member/member-schame'; await this.page.goto(customerPage); await expect(this.page.getByRole('button', { name: '新增顾客' })).toBeEnabled(); await this.selectStore(customer.store); await this.fillCustomerDetails(customer); await this.selectDepartment(customer); if (customer.employees.length !== 0) { await this.selectEmployee(customer.employees); } await this.selectGender(customer.gender); if (customer.birthday) { await this.selectBirthday(customer.birthday); } await this.selectSource(customer.source); await this.fillRemark(customer.remark); await this.confirmCreation(customer.phone); await this.page.locator('.person_content').waitFor(); console.log(`username:${customer.username}, phone:${customer.phone}创建成功`); await this.closeCustomerDetail(); }; /** * 选择门店 * @param store 门店 */ private readonly selectStore = async (store: number) => { await this.page.locator('.search_store > div').click(); await this.page.getByText('部门', { exact: true }).waitFor(); await this.page.locator('.shopSelect_box .shopSelect_shop_content').click(); await this.page .locator('.com_picker .label') .nth(store - 1) .click(); await this.page.getByRole('button', { name: /保\s存/ }).click(); await this.page.getByRole('button', { name: '新增顾客' }).click(); }; /** * 填写顾客信息 * @param customer 顾客 */ private readonly fillCustomerDetails = async (customer: Customer) => { await this.$register.getByPlaceholder('请输入姓名').fill(customer.username); await this.$register.getByPlaceholder('请输入会员手机号').fill(customer.phone); await this.$register.getByPlaceholder('请输入会员手机号').click(); const checkPhoneLocator = this.$register .locator('.ant-form-item', { hasText: '手机号' }) .locator('.ant-form-explain'); if (await checkPhoneLocator.isVisible()) { const phoneStr = await checkPhoneLocator.innerText(); if (phoneStr.includes('非法手机号码') || phoneStr.includes('请输入会员手机号')) { throw new Error(`手机号码:${customer.phone}不正确`); } } if (customer.archive) { await this.$register.getByPlaceholder('请输入12位以内的数字或字母').fill(customer.archive); } }; /** * 选择部门 * @param customer 顾客 */ private readonly selectDepartment = async (customer: Customer) => { await this.$register.locator('#register_departmentNo').getByRole('combobox').click(); if (customer.store === 1) { if (customer.department === 1) { await this.page.getByRole('option', { name: this.firstStore.firstDepartment.name }).click(); } else if (customer.department === 2) { await this.page.getByRole('option', { name: this.firstStore.secondDepartment.name }).click(); } } else if (customer.store === 2) { if (customer.department === 1) { await this.page.getByRole('option', { name: this.secondStore.firstDepartment.name }).click(); } else { throw new Error(`部门:${customer.department}不存在`); } } else { throw new Error(`门店:${customer.store}不存在`); } }; /** * 选择员工 * @param employees 员工 */ private readonly selectEmployee = async (employees: employee[]) => { await this.$register.locator('.ant-form-item', { hasText: '员工' }).getByRole('list').click(); for (const employee of employees) { await this.page .getByRole('treeitem', { name: employee.level }) .getByRole('treeitem', { name: employee.name }) .click(); } await this.page.getByRole('button', { name: '确认分配' }).click(); }; /** * 选择性别 * @param gender 性别 */ private readonly selectGender = async (gender: number) => { // 定义性别选项的映射 const GENDER_OPTIONS = { FEMALE: 0, MALE: 1, }; // 定义对应的文本 const GENDER_TEXTS = { [GENDER_OPTIONS.FEMALE]: '女性', [GENDER_OPTIONS.MALE]: '男性', }; if (gender === 0) { return; } await this.$register.locator('label').filter({ hasText: GENDER_TEXTS[gender] }).click(); }; /** * 选择生日 * @param {object} birthday 生日 * @param {number} birthday.year 年份 * @param {number} birthday.month 月份 * @param {number} birthday.day 日期 */ private readonly selectBirthday = async (birthday: { year: number; month: number; day: number }) => { if (!birthday) { throw new Error('birthday参数为空'); } const { year, month, day } = birthday; if (!Number.isInteger(year) || year < 1900 || year > new Date().getFullYear()) { throw new Error(`年份 ${year} 不合法`); } if (!Number.isInteger(month) || month < 1 || month > 12) { throw new Error(`月份 ${month} 不合法`); } if (!Number.isInteger(day) || day < 1 || day > 31) { throw new Error(`日期 ${day} 不合法`); } const $birthday = this.$register.locator('.ant-form-item', { hasText: '生日' }); if (year) { await $birthday.getByText('年份').click(); await this.page.getByRole('option', { name: `${year}` }).click(); await expect(this.page.getByRole('option', { name: `${year}` })).not.toBeVisible(); } if (month && day) { await $birthday.getByText('日期').click(); await this.page.getByRole('option', { name: new RegExp(`^${month}\\s月$`) }).click(); await this.page.getByRole('option', { name: new RegExp(`^${day}\\s日$`) }).click(); await this.page.getByRole('button', { name: /确\s认/ }).click(); } else { throw new Error(`month:${month}, day:${day}其中一个为空`); } }; /** * 选择顾客来源 * @param {number} source 来源 */ private readonly selectSource = async (source: number) => { await this.$register.getByLabel(this.sourceChannel[source]).click(); }; /** * 填写备注 * @param {string} remark 备注 */ private readonly fillRemark = async (remark: string) => { if (!remark) return; await this.$register.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); }; /** * 确认创建 * @param {string} phone 手机号 */ private readonly confirmCreation = async (phone: string) => { try { // 等待响应并点击按钮 const [response] = await Promise.all([ this.page.waitForResponse(async (res) => { const urlMatch = res.url().includes('/invalid_check'); if (!urlMatch) return false; try { const json = await res.json(); return json.code === 'SUCCESS'; } catch (e) { console.error('解析响应体失败:', e); return false; } }), this.page.getByRole('button', { name: '确认创建' }).click(), ]); // 解析响应体 let responseBody: any; try { responseBody = await response.json(); } catch (e) { throw new Error('无法解析服务器响应体,请检查网络连接或服务器状态'); } // 检查响应体内容 if (responseBody && typeof responseBody.content === 'object') { const phoneStatus = responseBody.content?.status; if (phoneStatus != null) { throw new Error(`手机号码 ${phone} 已被使用,无法创建新顾客`); } } else { console.warn('响应体格式不符合预期,content 不是对象'); } } catch (error) { // 捕获并记录异常 console.error('检查手机号码时发生错误:', error); throw error; // 重新抛出异常以便调用方处理 } // if (phoneStatus === undefined || phoneStatus === null) { // 创建顾客成功 // return; // } // await this.page.getByText('系统查询到当前手机号被建档后转为无效客,是否要恢复无效客?').waitFor(); // await this.page.getByRole('button', { name: '重新建档' }).click(); // const popupWindow = this.page.locator('.ant-message'); // await expect(popupWindow).not.toContainText('该手机号码已经被使用'); // await popupWindow.waitFor(); // const popupContent = (await popupWindow.innerText()).trim(); // if (popupContent.includes('该手机号码已经被使用')) { // throw new Error(`手机号码 ${phone} 已被使用,无法创建新顾客`); // } }; /** * 设置顾客为无效客 * @param {Customer} customer 顾客 */ setInvalidCustomer = async (customer: Customer) => { await this.page.getByRole('button', { name: '新增顾客' }).waitFor(); // 根据手机号进行搜索,进入顾客详情页面 await this.page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); await this.page.locator('.ant-input-suffix .search_btn', { hasText: '搜索' }).click(); await this.page.locator('.custom_content', { hasText: customer.phone }).click(); await this.page.locator('.m-table__fixed-left').getByText(customer.username).first().click(); // 设置无效客 await this.page.locator('.person_content .tag_box .more_icon svg').click(); await this.page.getByRole('menuitem', { name: '设为无效客' }).click(); const [response] = await Promise.all([ this.page.waitForResponse( response => response.url().includes('/customer') && response.request().method() === 'PATCH', ), this.page.getByRole('button', { name: /确\s认/ }).click(), ]); const responseBody = await response.json(); const code = responseBody?.code; expect(code).toBe('SUCCESS'); }; /** * 批量创建顾客 * @param customerArray */ createMoreCustomer = async (customerArray: Customer[]) => { for (const customer of customerArray) { await this.createCustomer(customer); } }; /** * 批量设置无效客 * @param customerArray */ setMoreInvalidCustomer = async (customerArray: Customer[]) => { for (const customer of customerArray) { await this.page.reload(); await this.setInvalidCustomer(customer); } }; }