Compare commits

...

20 Commits

Author SHA1 Message Date
d44f457d86 test(boss_cashier): 优化挂单测试流程
- 移除不必要的挂单数量记录步骤
- 简化挂单流程,每个员工只挂单一次
- 使用 expect.soft 进行软断言,提高测试稳定性
- 优化员工选择和挂单校验的代码结构
2025-03-23 11:54:08 +08:00
356a048508 ci: 为 Playwright 测试添加超时限制和报告生成
- 在 GitLab CI 配置文件中为 Playwright 测试阶段添加 60 分钟的超时限制
- 在测试完成后生成报告,无论测试是否成功
- 优化测试报告的生成和存储,确保在所有情况下都能生成报告
2025-03-22 12:18:48 +08:00
52365501f0 test(touch): 优化挂单测试流程
- 移除了 playwright 测试命令中的 --grep 参数
- 重构了员工选择和挂单操作的代码,提高了可读性和可维护性
- 优化了等待和点击操作,确保测试流程更加稳定
- 删除了冗余的代码,简化了测试逻辑
2025-03-21 23:26:22 +08:00
3f45ac233f ci: 修改GitLab CI配置
- 移除了playwright_tests阶段的artifacts配置中的一个路径
- 保留了test-results/目录,删除了playwright-report/目录
- 保留了artifacts的expire_in设置为1周
2025-03-17 23:40:13 +08:00
554ea2408d ci: 保存 Playwright 报告
- 在 .gitlab-ci.yml 文件中,将 playwright-report 目录添加到 artifacts 路径
- 这样可以确保测试报告在 CI/CD 流程中被正确保存和显示
2025-03-17 23:03:47 +08:00
88eae96f63 ci: 添加测试失败消息发送功能并调整 trace 配置
- 在 .gitlab-ci.yml 中添加 after_script 部分,根据测试结果发送相应消息
- 在 playwright.config.js 中将 trace 配置从 'retain-on-failure' 改为 'on',以收集所有测试的 trace 信息
2025-03-17 22:53:57 +08:00
bac0dbfdbe ci(test): 更新 Playwright 版本并优化测试脚本
- 将 Playwright 版本从 v1.43.0 升级到 v1.51.0
- 优化了 baseFixture 中的登录流程
- 改进了 customerPage 中的顾客创建逻辑
- 调整了 boss_cashier 和 staff_goal 测试中的操作方式
2025-03-16 19:10:03 +08:00
Administrator
fad6c46850 更新.gitlab-ci.yml文件 2025-03-13 14:18:50 +00:00
Administrator
86311180c2 更新.gitlab-ci.yml文件 2025-03-13 14:18:03 +00:00
Administrator
98962f3e31 更新.gitlab-ci.yml文件 2025-03-13 14:12:34 +00:00
Administrator
b2911faee1 更新.gitlab-ci.yml文件 2025-03-13 12:53:15 +00:00
Administrator
7accd0ce75 更新.gitlab-ci.yml文件 2025-03-13 12:00:58 +00:00
Administrator
afe574d2ca 更新.gitlab-ci.yml文件 2025-03-13 11:59:38 +00:00
fadfbe60fc ci(gitlab): 更新 Playwright 镜像版本并优化 CI 配置 2025-03-13 19:55:22 +08:00
e2df441b75 ci(gitlab): 移除 sudo 安装 Playwright 依赖的步骤 2025-03-13 19:38:38 +08:00
91e31e348e ci(gitlab): 在 CI 配置中使用 sudo 安装 Playwright 依赖 2025-03-13 19:37:20 +08:00
439779dd56 ci(gitlab): 在 CI 配置中添加 Playwright 依赖安装步骤 2025-03-13 19:35:36 +08:00
93266d7aa8 ci(gitlab): 在 CI 配置中添加淘宝镜像以加速 npm 安装 2025-03-13 19:33:26 +08:00
56ca150b6e ci(gitlab): 在 Playwright 测试中添加 NVM 配置以使用特定 Node.js 版本 2025-03-13 19:30:46 +08:00
b86b14a9a4 ci(gitlab): 添加标签和限制条件以优化 CI 流程 2025-03-12 22:38:50 +08:00
9 changed files with 135 additions and 196 deletions

View File

@ -1,29 +1,30 @@
stages:
- test
- report
playwright_tests:
stage: test
image: mcr.microsoft.com/playwright:focal
image: mcr.microsoft.com/playwright:v1.51.0-noble
timeout: 60 minutes
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/
before_script:
- npm config set registry https://registry.npmmirror.com
script:
- npm install
- npx playwright install
- npx playwright test
- npx playwright test /touch/boss_cashier.spec.ts --project '慧来客touch(管理员身份) - Desktop Chrome'
after_script:
- |
if [ "$CI_JOB_STATUS" == "success" ]; then
echo "Tests passed! Sending success message..."
else
echo "Tests failed! Sending failure message..."
fi
- ls -la playwright-report/
artifacts:
paths:
- test-results/
- playwright-report/
expire_in: 1 week
send_report:
stage: report
image: alpine:latest
script:
- apk add --no-cache curl
- |
if [ -d test-results ]; then
echo "Test Successful."
else
echo "No test results found."
fi
when: always
tags:
- test
only:
- main

View File

@ -17,7 +17,7 @@
"license": "ISC",
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@playwright/test": "^1.48.2",
"@playwright/test": "^1.51.1",
"@types/node": "^22.3.0",
"axios": "^1.7.4",
"cross-env": "^7.0.3"
@ -31,7 +31,7 @@
"ldap": "^0.7.1",
"ldap-authentication": "^3.2.2",
"ldapjs": "^3.0.7",
"playwright": "^1.47.2",
"playwright": "^1.51.1",
"qrcode-reader": "^1.0.4",
"sharp": "^0.33.5",
"tesseract.js": "^5.1.1",

View File

@ -55,7 +55,7 @@ module.exports = defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
hasTouch: true,
trace: 'retain-on-failure',
trace: 'on',
screenshot: 'only-on-failure',
timezoneId: 'Asia/Shanghai',
locale: 'zh-CN',
@ -87,7 +87,7 @@ module.exports = defineConfig({
...devices['Desktop Chrome'],
baseURL: process.env.BASE_URL,
storageState: firstAuthFile,
viewport: { width: 1280, height: 720 },
// viewport: { width: 1280, height: 720 },
isMobile: true,
},
testMatch: /.*boss_.*\.spec\.ts/,
@ -99,7 +99,7 @@ module.exports = defineConfig({
...devices['Desktop Safari'],
baseURL: process.env.BASE_URL,
storageState: firstAuthFile,
viewport: { width: 1280, height: 720 },
// viewport: { width: 1280, height: 720 },
isMobile: true,
},
testMatch: /.*boss_.*\.spec\.ts/,
@ -110,7 +110,7 @@ module.exports = defineConfig({
use: {
...devices['Desktop Chrome'],
baseURL: process.env.BASE_URL,
viewport: { width: 1280, height: 720 },
// viewport: { width: 1280, height: 720 },
isMobile: false,
},
testMatch: /.*staff_.*\.spec\.ts/,
@ -121,7 +121,7 @@ module.exports = defineConfig({
use: {
...devices['Desktop Safari'],
baseURL: process.env.BASE_URL,
viewport: { width: 1280, height: 720 },
// viewport: { width: 1280, height: 720 },
isMobile: false,
},
testMatch: /.*staff_.*\.spec\.ts/,

View File

@ -14,8 +14,13 @@ export const test = base.extend<MyFixture>({
await expect(page.locator('.versionModal_main_content')).toBeVisible();
});
if (!baseURL) throw new Error('baseURL is required');
await page.goto(baseURL);
try {
await page.goto(baseURL!, { timeout: 90 * 1000 });
} catch (error) {
console.log(error);
// await page.screenshot({ path: 'error.png' });
await page.reload();
}
// await page.getByRole('button', { name: /开\s单/ }).waitFor();
await expect(page.getByRole('button', { name: /开\s单/ })).toBeEnabled();
@ -24,7 +29,9 @@ export const test = base.extend<MyFixture>({
await page.getByRole('button', { name: /开\s单/ }).click();
// await page.getByRole('button', { name: /开\s单/ }).waitFor();
await expect(page.getByRole('button', { name: '创建会员' })).toBeEnabled();
await page.locator('.top > .anticon').first().click();
await expect(page.getByRole('button', { name: '创建会员' })).not.toBeVisible();
await expect(page.getByRole('button', { name: /开\s单/ })).toBeInViewport();
await use(page);
},

View File

@ -339,22 +339,55 @@ export class CustomerPage {
* @param {string} phone
*/
private readonly confirmCreation = async (phone: string) => {
try {
// 等待响应并点击按钮
const [response] = await Promise.all([
this.page.waitForResponse(async res => res.url().includes('/invalid_check') && (await res.json()).code === 'SUCCESS'),
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(),
]);
const responseBody = await response.json();
const phoneStatus = responseBody?.content?.status;
if (phoneStatus === undefined || phoneStatus === null) {
// 创建顾客成功
return;
// 解析响应体
let responseBody: any;
try {
responseBody = await response.json();
} catch (e) {
throw new Error('无法解析服务器响应体,请检查网络连接或服务器状态');
}
await this.page.getByText('系统查询到当前手机号被建档后转为无效客,是否要恢复无效客?').waitFor();
await this.page.getByRole('button', { name: '重新建档' }).click();
const popupWindow = this.page.locator('.ant-message');
await expect(popupWindow).not.toContainText('该手机号码已经被使用');
// 检查响应体内容
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();

View File

@ -18,7 +18,13 @@ for (let testAccount of testAccountArray) {
.locator('div', { has: page.getByRole('textbox', { name: '请输入您的手机号码' }) })
.locator('.pass_svg');
await page.goto(baseURL!);
try {
await page.goto(baseURL!, { timeout: 90 * 1000 });
} catch (error) {
console.log(error);
await page.bringToFront();
await page.reload();
}
await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(account);
await page.getByRole('textbox', { name: '请输入登录密码' }).fill(password);
await page.getByLabel('请同意慧来客隐私政策和用户协议').check();
@ -41,6 +47,6 @@ for (let testAccount of testAccountArray) {
await page.getByRole('button', { name: /登\s录/ }).click();
await page.getByRole('button', { name: /开\s单/ }).waitFor();
}
await page.context().storageState({ path: authFile });
await page.context().storageState({ path: authFile, indexedDB: true });
});
}

View File

@ -31,7 +31,7 @@ setup.describe('员工登录', () => {
await page.getByLabel('请同意慧来客隐私政策和用户协议').check();
await $login.click();
await expect(page.getByRole('button', { name: /开\s单/ })).toBeEnabled();
await page.context().storageState({ path: e.authFile });
await page.context().storageState({ path: e.authFile, indexedDB: true });
});
}
});

View File

@ -37,31 +37,15 @@ test.describe('挂单', () => {
});
test('查看员工名下挂单', async ({ page, customerPage }) => {
let OrderLQ = 0;
let OrderZP = 0;
await test.step('记录员工原挂单数量', async () => {
// 点击底部员工刘强
const EmployeeLQ = page.locator('.userpanel_item').getByText(Employees.FirstShop.Employee_8.name);
if (await EmployeeLQ.isVisible()) {
await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_8.name }).click();
await page.locator('.item_box').first().waitFor();
OrderLQ = await page.locator('.item_box').count();
} else {
OrderLQ = 0;
}
const $employeeBottom = page.locator('.userpanel_item');
const employeeLq = $employeeBottom.getByText(Employees.FirstShop.Employee_8.name);
const employeeZp = $employeeBottom.getByText(Employees.FirstShop.Employee_9.name);
// 点击底部员工周萍
const EmployeeZP = page.locator('.userpanel_item', { hasText: Employees.FirstShop.Employee_9.name });
if (await EmployeeZP.isVisible()) {
await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_9.name }).click();
await page.locator('.item_box').first().waitFor();
OrderZP = await page.locator('.item_box').count();
} else {
OrderZP = 0;
}
});
const $employeeLq = page.locator('.counselor:nth-child(2)').locator('.check_item:nth-child(1)').filter({ hasText: Employees.FirstShop.Employee_8.name });
await test.step('刘强挂单1', async () => {
const $$pendingOrderCard = page.locator('.cash_content .list .item_box');
await test.step('刘强挂单', async () => {
//员工1开第1单
await page.getByRole('button', { name: /^开\s单$/ }).click();
await customerPage.searchCustomer(phone);
@ -71,61 +55,18 @@ test.describe('挂单', () => {
// 选择添加员工
await page.locator('button.staff_btn').click();
// 选择员工:刘强
await page
.locator('.counselor:nth-child(2)')
.locator('.check_item:nth-child(1)')
.filter({ hasText: Employees.FirstShop.Employee_8.name })
.click();
await $employeeLq.click();
// 保存并复制到其他项目
await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click();
// 挂单
await page.locator('#cart_bottom_btn').getByText('挂单').click();
await expect.soft($$pendingOrderCard.first()).toContainText(`${username}`);
await expect($$pendingOrderCard.first()).toContainText(`${Employees.FirstShop.Employee_8.name}`);
});
await test.step('刘强挂单2', async () => {
//员工1开第2单
await page.getByRole('button', { name: /^开\s单$/ }).click();
await customerPage.searchCustomer(phone);
await customerPage.selectSearchCustomer(username);
const $employeeZp = page.locator('.counselor:nth-child(2)').locator('.check_item:nth-child(1)').filter({ hasText: Employees.FirstShop.Employee_9.name });
// 选择第三个项目
await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_3.name }).click();
// 选择添加员工
await page.locator('button.staff_btn').click();
// 选择员工:刘强
await page
.locator('.counselor:nth-child(2)')
.locator('.check_item:nth-child(1)')
.filter({ hasText: Employees.FirstShop.Employee_8.name })
.click();
// 保存并复制到其他项目
await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click();
// 挂单
await page.locator('#cart_bottom_btn').getByText('挂单').click();
});
await test.step('刘强挂单3', async () => {
//员工1开第3单
await page.getByRole('button', { name: /^开\s单$/ }).click();
await customerPage.searchCustomer(phone);
await customerPage.selectSearchCustomer(username);
// 选择第二个项目
await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_2.name }).click();
// 选择添加员工
await page.locator('button.staff_btn').click();
// 选择员工:刘强
await page
.locator('.counselor:nth-child(2)')
.locator('.check_item:nth-child(1)')
.filter({ hasText: Employees.FirstShop.Employee_8.name })
.click();
// 保存并复制到其他项目
await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click();
// 挂单
await page.locator('#cart_bottom_btn').getByText('挂单').click();
});
await test.step('周萍挂单1', async () => {
await test.step('周萍挂单', async () => {
//员工2开第1单
await page.getByRole('button', { name: /^开\s单$/ }).click();
await customerPage.searchCustomer(phone);
@ -137,54 +78,25 @@ test.describe('挂单', () => {
// 选择添加员工
await page.locator('button.staff_btn').click();
// 选择员工:周萍
await page
.locator('.counselor:nth-child(2)')
.locator('.check_row:nth-child(2) .check_item:nth-child(1)')
.filter({ hasText: Employees.FirstShop.Employee_9.name })
.click();
await $employeeZp.click();
// 保存并复制到其他项目
await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click();
// 挂单
await page.locator('#cart_bottom_btn').getByText('挂单').click();
await expect.soft($$pendingOrderCard.first()).toContainText(`${username}`);
await expect($$pendingOrderCard.first()).toContainText(`${Employees.FirstShop.Employee_9.name}`);
});
await test.step('周萍挂单2', async () => {
//员工2开第2单
await page.getByRole('button', { name: /^开\s单$/ }).click();
await customerPage.searchCustomer(phone);
await customerPage.selectSearchCustomer(username);
// 选择第二个项目
await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_2.name }).click();
// 选择添加员工
await page.locator('button.staff_btn').click();
// 选择员工:周萍
await page
.locator('.counselor:nth-child(2)')
.locator('.check_row:nth-child(2) .check_item:nth-child(1)')
.filter({ hasText: Employees.FirstShop.Employee_9.name })
.click();
// 保存并复制到其他项目
await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click();
// 挂单
await page.locator('#cart_bottom_btn').getByText('挂单').click();
// 等待挂单成功
await page.locator('.ant-message').waitFor();
// 判断挂单成功弹窗消失后再执行下一步
await expect(page.locator('.ant-message')).not.toBeVisible();
});
await test.step('最后校验挂单数量', async () => {
await test.step('校验各个员工的挂单', async () => {
// 点击底部员工刘强
await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_8.name }).click();
await page.locator('.item_box').first().waitFor();
const order = await page.locator('.item_box').all();
expect(order.length).toBe(3 + OrderLQ);
await employeeLq.click();
await expect.soft($$pendingOrderCard.first()).toContainText(`${username}`);
await expect($$pendingOrderCard.first()).toContainText(`${Employees.FirstShop.Employee_8.name}`);
// 点击底部员工周萍
await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_9.name }).click();
await page.locator('.item_box').first().waitFor();
const order1 = await page.locator('.item_box').all();
expect(order1.length).toBe(2 + OrderZP);
await employeeZp.click();
await expect.soft($$pendingOrderCard.first()).toContainText(`${username}`);
await expect($$pendingOrderCard.first()).toContainText(`${Employees.FirstShop.Employee_9.name}`);
});
});
@ -211,32 +123,18 @@ test.describe('挂单', () => {
// // 点击底部员工吴浩
// 取单
await page.locator('.item_box', { hasText: username }).waitFor();
await page
.locator('.item_box')
.first()
.getByRole('button', { name: /^取\s单$/ })
.click();
await page.locator('.item_box').first().getByRole('button', { name: /^取\s单$/ }).click();
await page.locator('#buyList').getByText('雪肌晶纯护理').waitFor();
//结算
await page
.locator('.pay_btn')
.filter({ hasText: /^结\s算$/ })
.click();
await page.locator('.pay_btn').filter({ hasText: /^结\s算$/ }).click();
//点击优惠券折扣
await page.getByText('优惠券抵扣').click();
//点击赠送优惠券
await page.getByText('赠送优惠券').click();
//点击定额10元券
await page
.locator('.popup_content')
.locator('.alloytouch-target')
.filter({ hasText: Coupons.coupon.coupon_1.name })
.click();
await page.locator('.popup_content').locator('.alloytouch-target').filter({ hasText: Coupons.coupon.coupon_1.name }).click();
//确认
await page
.locator('.operation_btn')
.first()
.getByRole('button', { name: /^确\s定$/ })
.click();
await page.locator('.operation_btn').first().getByRole('button', { name: /^确\s定$/ }).click();
//选择定额10元券
await page.getByLabel(Coupons.coupon.coupon_1.name).first().check();
//确认选择
@ -276,27 +174,21 @@ test.describe('挂单', () => {
});
test('已过期单据不能取单,只能删除', async ({ page }) => {
const $slidingMenu = page.locator('div.deleteList .m_sliding_menu');
await page.getByText('已过期 / 删除单据').click();
await page.getByText('警告:已过期服务无法取单结算,请至收银台处理').waitFor();
await $slidingMenu.locator('div.item_box').first().waitFor();
await expect(
$slidingMenu
.locator('div.item_box')
.first()
.getByRole('button', { name: /^取\s单$/ }),
).not.toBeVisible();
const $delete = $slidingMenu
.locator('div.item_box')
.first()
.locator('.comment > div:nth-child(2) > .touchIcon');
await $delete.click();
const $firstOrder = page.locator('.deleteList .m_sliding_menu .item_box').first();
await expect($firstOrder.getByRole('button', { name: /^取\s单$/ })).not.toBeVisible();
await $firstOrder.locator('.comment > div:nth-child(2) > .touchIcon').click();
await page.getByPlaceholder('请输入1-100个字符备注内容').click();
await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试备注');
await page.getByRole('button', { name: /^保\s存$/ }).click();
await expect(page.locator('.ant-message').filter({ hasText: '删除成功' }).nth(2)).toBeVisible();
await Promise.all([
page.getByRole('button', { name: /^保\s存$/ }).click(),
expect(page.locator('.ant-message').filter({ hasText: '删除成功' })).toBeVisible(),
]);
});
});
@ -306,7 +198,7 @@ test.describe('挂单', () => {
const phone = customer.phone;
const username = customer.username;
await page.pause();
// await page.pause();
await homeNavigation.gotoModule('收银');
await page.getByRole('button', { name: /^开\s单$/ }).click();

View File

@ -48,7 +48,7 @@ test.describe('目标-目标设置', () => {
.locator('.m-table-cell')
.nth(1)
.click();
await numberInput.setValue(Number(goal));
await numberInput.setValue('common', Number(goal));
await numberInput.confirmValue();
await page.getByRole('button', { name: '保 存' }).click();
@ -101,7 +101,7 @@ test.describe('目标-目标设置', () => {
.locator('.m-table-cell')
.nth(1)
.click();
await numberInput.setValue(0);
await numberInput.setValue('common', 0);
await numberInput.confirmValue();
await page.getByRole('button', { name: '保 存' }).click();
});
@ -191,7 +191,7 @@ test.describe('目标-目标设置', () => {
const code = page.locator('.popupComTableStyle .m-table__body .main-table-body_tr');
await code.nth(nowRowB).locator('.m-table-cell').nth(1).click();
await numberInput.setValue(Number(goalB));
await numberInput.setValue('common', Number(goalB));
await numberInput.confirmValue();
// 员工C内行数
@ -217,7 +217,7 @@ test.describe('目标-目标设置', () => {
).not.toBeVisible();
await code.nth(nowRowC).locator('.m-table-cell').nth(1).click();
await numberInput.setValue(Number(goalC));
await numberInput.setValue('common', Number(goalC));
await numberInput.confirmValue();
await page.getByRole('button', { name: /保\s存/ }).click();