- 将 Playwright 版本从 v1.43.0 升级到 v1.51.0 - 优化了 baseFixture 中的登录流程 - 改进了 customerPage 中的顾客创建逻辑 - 调整了 boss_cashier 和 staff_goal 测试中的操作方式
447 lines
17 KiB
TypeScript
447 lines
17 KiB
TypeScript
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);
|
||
}
|
||
};
|
||
}
|