优化代码

This commit is contained in:
LingandRX 2024-12-29 21:51:02 +08:00
parent 8c910d9086
commit 28430d0c9b
13 changed files with 211 additions and 230 deletions

View File

@ -2,6 +2,7 @@
const { defineConfig, devices } = require('@playwright/test');
// import dotenv from "dotenv";
import path from 'path';
import { firstAccount, secondAccount } from './tests/common/auth';
/**
* Read environment variables from file.
@ -15,9 +16,9 @@ require('dotenv').config({ path: path.resolve(__dirname, '.env') });
// ".env" + `${process.env.NODE_ENV ? "." + process.env.NODE_ENV : ""}`
// ),
// });
const authPath = '.auth/user.json';
const firstAuthFile = '.auth/admin_first.json';
const secondAuthFile = '.auth/admin_second.json';
const firstAuthFile = firstAccount.authFile;
const secondAuthFile = secondAccount.authFile;
/**
* @see https://playwright.dev/docs/test-configuration
*/

52
tests/common/auth.ts Normal file
View File

@ -0,0 +1,52 @@
import { getEnvVar } from '@/utils/envUtils';
import { readFileSync } from 'fs';
const authConfig = {
authFilePath: '.auth/',
indexedDBFilePath: '.auth/',
};
class AuthAccount {
account: string;
password: string;
authFile: string;
indexedDBFile: string;
constructor(account: string, password: string, authFile: string, indexedDBFile: string) {
this.account = account;
this.password = password;
this.authFile = authFile;
this.indexedDBFile = indexedDBFile;
}
static loadIndexedDBFile(account: string, accountArray: AuthAccount[]) {
try {
for (const item of accountArray) {
if (item.account === account) {
return JSON.parse(readFileSync(item.indexedDBFile, 'utf-8'));
}
}
} catch (e) {
throw new Error('indexedDB文件读取失败');
}
throw new Error('未找到该账户');
}
}
const firstAccount = new AuthAccount(
getEnvVar('boss_account'),
getEnvVar('boss_password'),
`${authConfig.authFilePath}${getEnvVar('boss_account')}.json`,
`${authConfig.indexedDBFilePath}${getEnvVar('boss_account')}_indexedDB.json`,
);
const secondAccount = new AuthAccount(
getEnvVar('boss_account_2'),
getEnvVar('boss_password_2'),
`${authConfig.authFilePath}${getEnvVar('boss_account_2')}.json`,
`${authConfig.indexedDBFilePath}${getEnvVar('boss_account_2')}_indexedDB.json`,
);
export { firstAccount, secondAccount, AuthAccount };

View File

@ -1,7 +1,7 @@
//@ts-check
// 默认为生产
let nodeEnv = process.env.NODE_ENV || "production";
nodeEnv = nodeEnv === "staging" ? "production" : nodeEnv;
let nodeEnv = process.env.NODE_ENV || 'production';
nodeEnv = nodeEnv === 'staging' ? 'production' : nodeEnv;
/**
* -
@ -14,163 +14,163 @@ nodeEnv = nodeEnv === "staging" ? "production" : nodeEnv;
let staffData = {
firstStore: {
firstSector: {
name: "美容部",
name: '美容部',
employee_1: {
name: "张伟",
phone: "13812345678",
name: '张伟',
phone: '13812345678',
id: { production: 3, test: 1 },
},
employee_2: {
name: "李娜",
phone: "13987654321",
name: '李娜',
phone: '13987654321',
id: { production: 4, test: 2 },
},
employee_3: {
name: "王芳",
phone: "13723456789",
name: '王芳',
phone: '13723456789',
id: { production: 5, test: 3 },
},
employee_4: {
name: "陈刚",
phone: "13698765432",
name: '陈刚',
phone: '13698765432',
id: { production: 6, test: 4 },
},
employee_5: {
name: "赵军",
phone: "13512349876",
name: '赵军',
phone: '13512349876',
id: { production: 7, test: 5 },
},
employee_6: {
name: "刘强",
phone: "13498761234",
name: '刘强',
phone: '13498761234',
id: { production: 8, test: 6 },
},
employee_7: {
name: "周萍",
phone: "13365432109",
name: '周萍',
phone: '13365432109',
id: { production: 9, test: 7 },
},
employee_8: {
name: "吴浩",
phone: "13287654329",
name: '吴浩',
phone: '13287654329',
id: { production: 10, test: 8 },
},
employee_9: {
name: "徐亮",
phone: "13123459876",
name: '徐亮',
phone: '13123459876',
id: { production: 11, test: 9 },
},
employee_10: {
name: "杨雪",
phone: "13098761234",
name: '杨雪',
phone: '13098761234',
id: { production: 12, test: 10 },
},
},
secondSector: {
name: "医美部",
name: '医美部',
employee_1: {
name: "赵伟",
phone: "13923456789",
name: '赵伟',
phone: '13923456789',
id: { production: 13, test: 11 },
},
employee_2: {
name: "钱丽",
phone: "13898765432",
name: '钱丽',
phone: '13898765432',
id: { production: 14, test: 12 },
},
employee_3: {
name: "孙峰",
phone: "13712349876",
name: '孙峰',
phone: '13712349876',
id: { production: 15, test: 13 },
},
employee_4: {
name: "李涛",
phone: "13687654321",
name: '李涛',
phone: '13687654321',
id: { production: 16, test: 14 },
},
employee_5: {
name: "周慧",
phone: "13598761234",
name: '周慧',
phone: '13598761234',
id: { production: 17, test: 15 },
},
employee_6: {
name: "吴凯",
phone: "13465432109",
name: '吴凯',
phone: '13465432109',
id: { production: 18, test: 16 },
},
employee_7: {
name: "郑翔",
phone: "13387654329",
name: '郑翔',
phone: '13387654329',
id: { production: 19, test: 17 },
},
employee_8: {
name: "冯敏",
phone: "13223459876",
name: '冯敏',
phone: '13223459876',
id: { production: 20, test: 18 },
},
employee_9: {
name: "朱强",
phone: "13198761234",
name: '朱强',
phone: '13198761234',
id: { production: 21, test: 19 },
},
employee_10: {
name: "何平",
phone: "13065432198",
name: '何平',
phone: '13065432198',
id: { production: 22, test: 20 },
},
},
},
secondStore: {
name: "美容部",
name: '美容部',
firstSector: {
employee_1: {
name: "张凯",
phone: "13865432198",
name: '张凯',
phone: '13865432198',
id: { production: 1, test: 1 },
},
employee_2: {
name: "李军",
phone: "13923459876",
name: '李军',
phone: '13923459876',
id: { production: 2, test: 2 },
},
employee_3: {
name: "王涛",
phone: "13798761234",
name: '王涛',
phone: '13798761234',
id: { production: 3, test: 3 },
},
employee_4: {
name: "陈敏",
phone: "13654321987",
name: '陈敏',
phone: '13654321987',
id: { production: 4, test: 4 },
},
employee_5: {
name: "赵峰",
phone: "13523456789",
name: '赵峰',
phone: '13523456789',
id: { production: 5, test: 5 },
},
employee_6: {
name: "刘丽",
phone: "13487654321",
name: '刘丽',
phone: '13487654321',
id: { production: 6, test: 6 },
},
employee_7: {
name: "周亮",
phone: "13398765432",
name: '周亮',
phone: '13398765432',
id: { production: 7, test: 7 },
},
employee_8: {
name: "吴平",
phone: "13212349876",
name: '吴平',
phone: '13212349876',
id: { production: 8, test: 8 },
},
employee_9: {
name: "徐浩",
phone: "13165432109",
name: '徐浩',
phone: '13165432109',
id: { production: 9, test: 9 },
},
employee_10: {
name: "孙杰",
phone: "13087654329",
name: '孙杰',
phone: '13087654329',
id: { production: 10, test: 10 },
},
},
@ -200,7 +200,7 @@ function init(staffData, nodeEnv) {
const store = updatedData[storeKey];
Object.keys(store).forEach(sectorKey => {
const sector = store[sectorKey];
if (typeof sector === "object" && sector !== null && sector.name) {
if (typeof sector === 'object' && sector !== null && sector.name) {
updateIds(sector); // 更新每个 sector 的员工 id
}
});

View File

@ -1,7 +1,7 @@
import { test as base, expect } from '@playwright/test';
import { HomeNavigation } from '@/pages/homeNavigationPage';
import { writeIndexedDB } from '@/utils/indexedDBUtils';
import { readFileSync } from 'fs';
import { firstAccount, secondAccount, AuthAccount } from '@/common/auth';
type MyFixture = {
homeNavigation: HomeNavigation;
@ -17,7 +17,6 @@ export const test = base.extend<MyFixture>({
if (!baseURL) throw new Error('baseURL is required');
await page.goto(baseURL);
// await page.getByRole('button', { name: /开\s单/ }).waitFor();
const mobileObject = await page.evaluate(() => {
return window.localStorage.getItem('hlk_touch_mobile');
@ -26,36 +25,26 @@ export const test = base.extend<MyFixture>({
throw new Error('localStorage does not contain boss_account or boss_account2');
}
const mobileString = JSON.parse(mobileObject);
const bossAccount = process.env.boss_account || '';
const bossAccount2 = process.env.boss_account2 || '';
let jsonData: [];
if (mobileString.val?.includes(bossAccount)) {
jsonData = JSON.parse(readFileSync('.auth/admin_first_indexeddb.json', 'utf-8'));
} else if (mobileString.val?.includes(bossAccount2)) {
jsonData = JSON.parse(readFileSync('.auth/admin_second_indexeddb.json', 'utf-8'));
} else {
throw new Error('localStorage does not contain boss_account or boss_account2');
}
const data = AuthAccount.loadIndexedDBFile(mobileString.val, [firstAccount, secondAccount]);
await page.evaluate(
async ({ fnString, jsonData }) => {
// 在浏览器上下文中动态创建函数
async ({ fnString, data }) => {
const writeIndexedDBFn = new Function('return ' + fnString)();
// 调用函数并传递参数
return await writeIndexedDBFn(jsonData);
return await writeIndexedDBFn(data);
},
{ fnString: writeIndexedDB.toString(), jsonData },
{ fnString: writeIndexedDB.toString(), data },
);
// await page.reload();
await page.waitForFunction(
async () => {
const databases = await indexedDB.databases(); // 获取所有数据库
return databases.some(db => db.name === 'hlk_touch_test_init'); // 判断是否存在目标数据库
},
{ timeout: 30000 }, // 设置超时时间,默认为 30 秒
{ timeout: 30000 },
);
await page.reload();
await page.getByRole('button', { name: /开\s单/ }).waitFor();
await use(page);

View File

@ -1,5 +1,6 @@
import { expect, type Page, type Locator } from '@playwright/test';
import { Customer } from '@/utils/customer';
import { getNextAppointmentTime } from '@/utils/timeUtils';
/**
*
@ -88,22 +89,6 @@ export class AppointmentPage {
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}`;
};
/**
*
*/
@ -159,7 +144,7 @@ export class AppointmentPage {
*
*/
openAppointmentCell = async (name: string, time?: Date, retry = 10) => {
const currentAppointment = time ? this.getAppointmentTimesAvailable(time) : this.getAppointmentTimesAvailable();
const currentAppointment = time ? getNextAppointmentTime(time) : getNextAppointmentTime();
const $currentTime = this.$$time.filter({ hasText: currentAppointment });
const currentTimeBoundingBox = await $currentTime.boundingBox();

View File

@ -1,37 +1,21 @@
import { test as setup } from '@playwright/test';
import { readIndexedDB } from '@/utils/indexedDBUtils';
import fs from 'fs';
import { firstAccount, secondAccount } from '@/common/auth';
const firstAuthFile = '.auth/admin_first.json';
const secondAuthFile = '.auth/admin_second.json';
const regex = /^https?:\/\/(www\.)?hlk\.meiguanjia\.net\/?(#\s)?$/;
const regex = /^https?:\/\/(?:www\.)?hlk\.meiguanjia\.net\/$/;
/** @param personalizedPages 营销-个性化页面*/
const personalizedPages = '/#/marketing/brand/personalized';
class testAccount {
constructor(account: string, password: string, path: string) {
this.account = account;
this.password = password;
this.path = path;
}
const testAccountArray = [firstAccount, secondAccount];
account: string;
password: string;
path: string;
}
const allAccounts = [
new testAccount(process.env.boss_account!, process.env.boss_password!, firstAuthFile),
new testAccount(process.env.boss_account_2!, process.env.boss_password_2!, secondAuthFile),
];
for (let a of allAccounts) {
setup(`租户${a.account}总部管理员登录`, async ({ page, baseURL }) => {
const account = a.account;
const password = a.password;
const savePath = a.path;
for (let testAccount of testAccountArray) {
setup(`租户${testAccount.account}总部管理员登录`, async ({ page, baseURL }) => {
const account = testAccount.account;
const password = testAccount.password;
const authFile = testAccount.authFile;
const indexedDBFile = testAccount.indexedDBFile;
const $phonePassIcon = page
.locator('div', { has: page.getByRole('textbox', { name: '请输入您的手机号码' }) })
@ -64,7 +48,7 @@ for (let a of allAccounts) {
const readIndexedDBFn = new Function('return ' + readIndexedDBFnString)();
return await readIndexedDBFn();
}, readIndexedDB.toString());
fs.writeFileSync('.auth/admin_first_indexeddb.json', JSON.stringify(result));
await page.context().storageState({ path: savePath });
fs.writeFileSync(indexedDBFile, JSON.stringify(result));
await page.context().storageState({ path: authFile });
});
}

View File

@ -1,5 +1,7 @@
import { test as setup, expect } from "@playwright/test";
const authFileArray = [".auth/user_1.json", ".auth/user_2.json"];
import { test as setup, expect } from '@playwright/test';
import { getEnvVar } from '@/utils/envUtils';
const authFileArray = ['.auth/user_1.json', '.auth/user_2.json'];
const employee = [
{
phone: process.env.staff1_account,
@ -10,21 +12,21 @@ const employee = [
password: process.env.staff2_password,
},
];
setup.describe("员工登录", () => {
setup.describe('员工登录', () => {
for (let i = 0; i < 2; i++) {
setup(`门店员工登录_${i}`, async ({ page }) => {
const baseUrl = process.env.BASE_URL;
const $phone = page.getByRole("textbox", { name: "请输入您的手机号码" });
const $password = page.getByRole("textbox", { name: "请输入登录密码" });
const $login = page.getByRole("button", { name: /登\s录/ });
const baseUrl = getEnvVar('BASE_URL');
const $phone = page.getByRole('textbox', { name: '请输入您的手机号码' });
const $password = page.getByRole('textbox', { name: '请输入登录密码' });
const $login = page.getByRole('button', { name: /登\s录/ });
await page.goto(baseUrl);
await $phone.fill(employee[i].phone);
const checkPhone = page.locator(".ant-row", { has: $phone }).locator(".pass_svg");
const checkPhone = page.locator('.ant-row', { has: $phone }).locator('.pass_svg');
await expect(checkPhone).toBeVisible();
await $password.fill(employee[i].password);
await page.getByLabel("请同意慧来客隐私政策和用户协议").check();
await page.getByLabel('请同意慧来客隐私政策和用户协议').check();
await $login.click();
await expect(page.getByRole("button", { name: /开\s单/ })).toBeEnabled();
await expect(page.getByRole('button', { name: /开\s单/ })).toBeEnabled();
await page.context().storageState({ path: authFileArray[i] });
});
}

View File

@ -1,24 +1,19 @@
// @ts-check
import { faker } from '@faker-js/faker/locale/zh_CN';
import { test, expect } from '@/fixtures/boss_common.js';
import { staffData } from '@/fixtures/staff.js';
import { HomeNavigation } from '@/pages/homeNavigationPage.js';
import { AppointmentPage } from '@/pages/appointmentPage.js';
import { AppointmentOperation } from '@/pages/appointmentPage.js';
import { staffData } from '@/common/staff';
import { AppointmentOperation } from '@/pages/appointmentPage';
import { getNextAppointmentTime } from '@/utils/timeUtils';
test('使用预约单元格', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => {
const employee = staffData.firstStore.firstSector.employee_1;
const setAppointmentTime = appointmentPage.getAppointmentTimesAvailable();
const appointmentTime = getNextAppointmentTime();
const customer = createCustomer;
/** 当前可预约时间定位器 */
const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime });
const $time = page.locator('.times_table td').filter({ hasText: appointmentTime });
/** 顾客预约定位器 */
const $customerAppointment = page
.locator('.a_userInfo')
.filter({ hasText: customer.phone })
.filter({ hasText: customer.username });
const $customerAppointment = page.locator('.a_userInfo').filter({ hasText: customer.phone }).filter({ hasText: customer.username });
await test.step('进行未指定预约', async () => {
await homeNavigation.gotoModule('预约');
@ -96,7 +91,7 @@ test('使用预约单元格', async ({ page, homeNavigation, createCustomer, app
test('占用预约单元格', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => {
// 获取当前可预约时间
let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable();
let appointmentTime = getNextAppointmentTime();
// 创建顾客
const customer = createCustomer;
@ -106,7 +101,7 @@ test('占用预约单元格', async ({ page, homeNavigation, createCustomer, app
const remark = '占用预约单元格' + faker.string.alpha(2);
// 当前可预约时间定位器
const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime });
const $time = page.locator('.times_table td').filter({ hasText: appointmentTime });
// 员工定位器
const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name });
// 顾客预约定位器
@ -202,18 +197,15 @@ test('占用预约单元格', async ({ page, homeNavigation, createCustomer, app
test.describe('预约状态', () => {
test('预约-挂单-结算', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage, billSet }) => {
// 员工4
const employee = staffData.firstStore.firstSector.employee_4;
// 使用的项目
const project = { no: '100018', name: '苹果精萃护理', shortName: '精萃护理', price: 980 };
// 获取当前可预约时间
const setAppointmentTime = appointmentPage.getAppointmentTimesAvailable();
const appointmentTime = getNextAppointmentTime();
const customer = createCustomer;
// 当前可预约时间定位器
const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime });
const $time = page.locator('.times_table td').filter({ hasText: appointmentTime });
// 员工定位器
const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name });
// 创建顾客
const customer = createCustomer;
// 获取设置的预约状态
let appointmentStatusSetting;
@ -311,74 +303,29 @@ test.describe('预约状态', () => {
});
});
test.afterEach(async ({ homeNavigation, wasteBookBusinessRecordPage, billSet }) => {
// 撤回预约开的单
await homeNavigation.gotoModule('流水');
await wasteBookBusinessRecordPage.revokeOrder(billSet);
});
// 清除当前时间之后所有的预约和占用
test.afterAll(async ({ browser, baseURL }) => {
const page = await browser.newPage();
await page.goto(baseURL ?? '');
const homeNavigation = new HomeNavigation(page);
const appointmentPage = new AppointmentPage(page);
test('测试预约操作', async ({ page, homeNavigation }) => {
await homeNavigation.gotoModule('预约');
await expect(page.getByText('张伟')).toBeVisible();
const $$userInfo = page.locator('.a_userInfo');
// 获取顾客占用预约单元格信息
const $$appointmentBox = $$userInfo.locator('.appointment_content');
await page.getByRole('button', { name: /设\s置/ }).waitFor();
await page.waitForTimeout(3000);
// await page.mouse.wheel(0, 500);
const names = await $$appointmentBox.locator('.name').allInnerTexts();
const userPhones = await $$appointmentBox.locator('.user_phone').allInnerTexts();
const $table = page.locator('.content_table');
const tableBox = await $table.boundingBox();
if (tableBox) {
const startX = tableBox.x + tableBox.width / 2; // 元素中心点 X 坐标
const startY = tableBox.y + tableBox.height / 2; // 元素中心点 Y 坐标
const userInfoData = names.map((name, index) => {
return {
name,
phone: userPhones[index],
};
});
// 移动鼠标到起始位置
await page.mouse.move(startX, startY);
// 取消预约占用
// 获取占用框的数量
let occupyBoxCount = await $$userInfo.locator('.occupy').count();
while (occupyBoxCount > 0) {
// 每次都使用 first() 获取第一个占用框
const $occupyBox = $$userInfo.locator('.occupy').first();
await $occupyBox.click();
await page
.locator('.popup_content .class_content')
.getByText(/^取消占用$/)
.click();
// 按下鼠标左键
await page.mouse.down();
try {
// 等待请求成功,确认按钮和响应并行处理
await Promise.all([
page.waitForResponse(res => res.url().includes('/reservation') && res.status() === 200, {
timeout: 5000,
}), // 添加超时
page.getByRole('button', { name: /确\s认/ }).click(),
]);
} catch (error) {
console.error('取消预约时出现错误: ', error);
}
// 向左拖动 (X 坐标减小Y 坐标保持不变)
await page.mouse.move(startX - 400, startY, { steps: 10 });
await page.waitForTimeout(500);
occupyBoxCount = await $$userInfo.locator('.occupy').count();
// 松开鼠标左键
await page.mouse.up();
}
// 取消有取消预约按钮的预约
for (const user of userInfoData) {
const { name } = user;
const $appointmentBox = $$appointmentBox.filter({ has: page.getByText(name) }).locator('.user_name_info');
const appointCount = await $appointmentBox.count();
if (appointCount === 1 && (await appointmentPage.elementCenterInViewport($appointmentBox.first()))) {
await $appointmentBox.first().click();
await appointmentPage.cancelAppoint();
}
}
await page.close();
});

View File

@ -1,6 +1,6 @@
// @ts-check
import { expect, test } from '@/fixtures/boss_common.js';
import { staffData } from '@/fixtures/staff.js';
import { staffData } from '@/common/staff';
import { Employees } from '@/fixtures/userconfig.js';
import { Customer } from '@/utils/customer';
import { KeepOnlyNumbers } from '@/utils/utils.js';

View File

@ -4,7 +4,7 @@ import { test, expect } from '@/fixtures/boss_common.js';
import path from 'path';
import { decodeQR } from '@/utils/utils.js';
import { Customer } from '@/utils/customer';
import { staffData } from '@/fixtures/staff.js';
import { staffData } from '@/common/staff';
import { HomeNavigation } from '@/pages/homeNavigationPage.js';
import fs from 'fs';
import { Page } from '@playwright/test';

View File

@ -6,7 +6,7 @@ import { KeepOnlyNumbers } from '@/utils/utils.js';
import fs from 'fs';
import path from 'path';
import { Employees, ProjectName } from '@/fixtures/userconfig.js';
import { staffData } from '@/fixtures/staff.js';
import { staffData } from '@/common/staff';
test.describe('营业记录', () => {
test.beforeEach(async ({ page }) => {

7
tests/utils/envUtils.ts Normal file
View File

@ -0,0 +1,7 @@
export function getEnvVar(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Environment variable ${key} is not set`);
}
return value;
}

14
tests/utils/timeUtils.ts Normal file
View File

@ -0,0 +1,14 @@
/**
*
* - 8:00 --> 8:30
* - 8:30 --> 9:00
* - 8:50 --> 9:00
*/
function getNextAppointmentTime(date = new Date()) {
const currentHour = date.getHours();
const nextTime = date.getMinutes() > 28 ? ':00' : ':30';
const hour = String(currentHour + (nextTime === ':00' ? 1 : 0)).padStart(2, '0');
return `${hour}${nextTime}`;
}
export { getNextAppointmentTime };