feat: 初始化慧来客自动化测试项目

- 添加项目配置文件和环境变量设置
- 创建测试用例目录结构和命名规范
- 实现基础测试 fixture 和页面对象模型
- 添加示例测试用例和数据生成器
- 配置 playwright 和 gitignore 文件
This commit is contained in:
LingandRX 2024-12-22 19:18:27 +08:00
commit 6517e4192c
74 changed files with 21749 additions and 0 deletions

22
.env Normal file
View File

@ -0,0 +1,22 @@
QY_WECHAT_ROOTBOT_URL='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1c7d2bf1-eae9-4ee9-bdfb-b578e747f2dc'
BASE_URL = 'https://hlk.meiguanjia.net/'
boss_account= "17770720220"
boss_password= "a123456"
boss_account_2= "1770720220"
boss_password_2= "a123456"
staff1_account= "17770720221"
staff1_password= "a123456"
staff2_account= "17770720222"
staff2_password= "a123456"
STOREH5_URL = 'https://hlk.meiguanjia.net/h5/#/?env=0&mid=14920&tv=qym&query=%257B%2522tab%2522%253A%2522MALL%2522%257D'
WEB_AUTH_USERNAME='mgj'
WEB_AUTH_PASSWORD='mgj123456'
DOMAIN_URL = 'https://autotest.meiguanjia.net'
SERVER_PORT = 9901
PROXY_SERVER_PORT = 9900
LDAP_BindURL = 'ldap://ldapadmin.meiguanjia.net:389'
LDAP_bindDN = 'cn=admin,dc=meiguanjia,dc=net'
LDAP_bindUserDN = 'ou=person,dc=meiguanjia,dc=net'
LDAPbindPSW = '!qaz2Wsxyjy0102'

21
.env.staging Normal file
View File

@ -0,0 +1,21 @@
BASE_URL = 'https://stghlk.meiguanjia.net/'
QY_WECHAT_ROOTBOT_URL='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1c7d2bf1-eae9-4ee9-bdfb-b578e747f2dc'
#QY_WECHAT_ROOTBOT_URL=''
boss_account= "17770720220"
boss_password= "a123456"
staff1_account= "17770720221"
staff1_password= "a123456"
staff2_account= "17770720222"
staff2_password= "a123456"
WEB_AUTH_USERNAME='mgj'
WEB_AUTH_PASSWORD='mgj123456'
DOMAIN_URL = 'https://autotest.meiguanjia.net'
SERVER_PORT = 9901
PROXY_SERVER_PORT = 9900
LDAP_BindURL = 'ldap://ldapadmin.meiguanjia.net:389'
LDAP_bindDN = 'cn=admin,dc=meiguanjia,dc=net'
LDAP_bindUserDN = 'ou=person,dc=meiguanjia,dc=net'
LDAPbindPSW = '!qaz2Wsxyjy0102'

7
.env.test Normal file
View File

@ -0,0 +1,7 @@
BASE_URL = 'https://prestghlk.meiguanjia.net/'
boss_account= "17770720220"
boss_password= "a123456"
staff1_account= "17770720221"
staff1_password= "a123456"
staff2_account= "17770720222"
staff2_password= "a123456"

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
node_modules/
/\.auth/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
package-lock.json
/.idea/

13
.prettierrc Normal file
View File

@ -0,0 +1,13 @@
{
"printWidth": 120,
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"proseWrap": "never",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto"
}

111
Readme.md Normal file
View File

@ -0,0 +1,111 @@
这是慧来客的自动化测试系统规范此项目支持图像化界面管理测试case自动化测试流水线发布且支持报告发送和错误现场回溯。如有不懂请联系WQ||Co!
### 支持的能力
- ✅图形化测试用例管理
- ✅多环境支持测试
- ✅多线程测试
- ✅多角色测试
- ✅测试报告生成
- ✅错误现场还原跟踪
- ✅测试过程录像
- ✅测试报告推送到企业微信
- ✅API接口测试
- ❌API压力测试
### 目录结构
```md
playwright_project/
├── .auth/ # 认证信息存放位置
├── node_modules/ # 存放 npm 包
├── playwright-report/ #测试报告存放目录
├── package.json # 项目配置文件
├── test-results/ #测试解过
├── playwright.config.ts # Playwright 配置文件
├── tests/ # 测试case文件存放目录
│ ├── touch/ # 慧来客touch端case的存放
│ │ ├── cashier_registration.spec.js # 挂单
│ │ ├── cashier_collection.spec.js #收款
│ │ └── ...
│ ├── mallh5/ # 慧来客客户端h5
│ │ ├── Mine_card.spec.js # 会员卡查看
│ │ └── ...
│ ├── business/ # 测试套件目录
│ │ ├── Member_search.spec.js # 搜索会员
│ │ └── ...
│ ├── fixtures/ # 测试公用套件
│ │ ├── user.json # 用户数据
│ │ └── cashier_set.json #收银数据设置
│ └── utils/ # 工具函数目录
│ ├── screenshotHelper.ts # 截图辅助函数
│ └── ...
└── tests-examples/ # 测试例子参考目录
│ └── ...
└── .env* # 环境变量存放位置,根据 process.env.NODE_ENV 读取不同的配置文件
└── my-awesome-reporter.ts # 自定义报告生成器用于推送
└── playwright.config* # 测试项目基础配置文件
```
[测试框架技术文档](https://playwright.nodejs.cn/docs/intro)
### 项目规范
1. case的存放地址/tests,所有的文件名应该让人看见名称知道case文件具体的含义使用 当前测试的角色_case一级菜单菜单命名如管理员身份的收银用例的文件命名应该为:cashier.spec.js。
- 总部管理员 boss
- 门店员工 staff
2. case的编写规范二级菜单下使用test.describe用二级菜单名字做分组然后二级菜单下的具体功能点则放在使用每一个test做声明。每一个功能点以具体的测试用例命名收银-挂单的例子如下。
```javascript
//收银挂单测试
test.describe('收银-挂单', () => {
//挂单
test('挂单', async ({ page }) => {
await page.getByRole('button', { name: '开 单' }).click();
await page.getByRole('button', { name: '1' }).click();
await page.getByText('搜索').click();
await page.getByLabel('icon: close').locator('svg').click();
await page.locator('.top > .anticon').first().click();
});
//取单
test('取单', async ({ page }) => {
await page.getByRole('button', { name: '开 单' }).click();
await page.getByRole('button', { name: '1' }).click();
await page.getByText('搜索').click();
await page.getByLabel('icon: close').locator('svg').click();
await page.locator('.top > .anticon').first().click();
});
})
```
3. 测试参数设置,项目启动会根据名 process.env.NODE_ENV 读取不同文件的配置.env.${环境变量}设置到当前启动线程中启动命令中使用cross-env设置环境变量。
4. 预设的npm命令和后期添加规则,命令命名为 作用-环境名
```shell
npm run prepared #安装必要的测试框架依赖
npm run codegen
npm run codegen-staging #启动staging录制
npm run ui #启动图形化管理界面
npm run ui-staging
npm run report #获取测试报告
npm run test #启动测试 全量测试
npm run test-staging #启动staging环境测试 全量测试
npm run ui-server #启动服务器图形化管理界面服务
npm run ui-server-staging #启动服务器图形化管理界面服务
```
5. 写case的过程中请尽量避免使用waitfor利用平台自身的等待机制
### 测试case索引
|cese名称|case地址|case名称|状态|最后时间|编写人|
|-|-|-|-|-|-|
|收银-挂单|touch/boo_cashier_registration.spec.js|cosavebill|✅|2024-08-16|co|
|收银-挂单|touch/boo_cashier_registration.spec.js|cosavebill|✅|2024-08-16|co|
|收银-挂单|touch/boss_cashier.spec.js|cosavebill|✅|2024-08-19|chenjihuang|
| | | | | | |
### 图形化管理工具
点击 测试管理工具.bat 启动
### case地址
[Mat用例点我打开](https://doc.weixin.qq.com/sheet/e3_AX8AngY-AOkP8BOfjAJQqCgCwFQQZ?scode=AKsAggcJABAAF0r0lLALgA3QZpAMU&tab=000001)

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "automated-testing",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"ui": "npx playwright test --ui",
"ui-staging": "cross-env NODE_ENV=staging npx playwright test --ui",
"test": "npx playwright test --ui --headed"
},
"repository": {
"type": "git",
"url": "https://git.meiguanjia.net/gitlab/hlk/automated-testing.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@playwright/test": "^1.48.2",
"@types/node": "^22.3.0",
"axios": "^1.7.4",
"cross-env": "^7.0.3"
},
"dependencies": {
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-session": "^1.18.0",
"http-proxy-middleware": "^3.0.0",
"jimp": "^0.22.12",
"ldap": "^0.7.1",
"ldap-authentication": "^3.2.2",
"ldapjs": "^3.0.7",
"playwright": "^1.47.2",
"qrcode-reader": "^1.0.4",
"sharp": "^0.33.5",
"tesseract.js": "^5.1.1",
"typescript": "^5.6.3"
}
}

136
playwright.config.js Normal file
View File

@ -0,0 +1,136 @@
// @ts-check
const { defineConfig, devices } = require('@playwright/test');
// import dotenv from "dotenv";
import path from 'path';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
require('dotenv').config({ path: path.resolve(__dirname, '.env') });
// dotenv.config({
// path: path.resolve(
// __dirname,
// ".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';
/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
name: '美管加自动测试框架',
timeout: 10 * 60 * 1000,
globalTimeout: 600 * 60 * 1000,
expect: {
timeout: 30 * 1000,
toPass: {
timeout: 30 * 1000,
},
},
testDir: './tests/',
snapshotPathTemplate: '{testDir}/imgs/__screenshots__/{testFilePath}/{arg}{ext}',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 1 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : 2,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [['html'], ['list'], ['./my-awesome-reporter.ts']],
// reporter: './my-awesome-reporter.ts',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 30 * 1000,
navigationTimeout: 60 * 1000,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
hasTouch: true,
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
timezoneId: 'Asia/Shanghai',
locale: 'zh-CN',
geolocation: { longitude: 114.24, latitude: 22.73 },
permissions: ['geolocation'],
extraHTTPHeaders: {
'Accept-Language': 'zh-CN,zh;q=0.9',
},
video: {
mode: 'retain-on-failure',
size: { width: 640, height: 480 },
},
},
/* Configure projects for major browsers */
projects: [
{
name: '总部管理员鉴权',
testMatch: /.*boss_auth\.setup\.js/,
use: { baseURL: process.env.BASE_URL },
},
{
name: '门店员工鉴权',
testMatch: /.*staff_auth\.setup\.js/,
use: { baseURL: process.env.BASE_URL },
},
{
name: '慧来客touch(管理员身份) - Desktop Chrome',
use: {
...devices['Desktop Chrome'],
baseURL: process.env.BASE_URL,
storageState: firstAuthFile,
viewport: { width: 1280, height: 720 },
isMobile: true,
},
testMatch: /.*boss_.*\.spec\.js/,
dependencies: ['总部管理员鉴权'],
},
{
name: '慧来客touch(管理员身份) - Desktop Safari',
use: {
...devices['Desktop Safari'],
baseURL: process.env.BASE_URL,
storageState: firstAuthFile,
viewport: { width: 1280, height: 720 },
isMobile: true,
},
testMatch: /.*boss_.*\.spec\.js/,
dependencies: ['总部管理员鉴权'],
},
{
name: '慧来客touch(门店员工身份) - Desktop Chrome',
use: {
...devices['Desktop Chrome'],
baseURL: process.env.BASE_URL,
viewport: { width: 1280, height: 720 },
isMobile: false,
},
testMatch: /.*staff_.*\.spec\.js/,
dependencies: ['门店员工鉴权'],
},
{
name: '慧来客touch(门店员工身份) - Desktop Safari',
use: {
...devices['Desktop Safari'],
baseURL: process.env.BASE_URL,
viewport: { width: 1280, height: 720 },
isMobile: false,
},
testMatch: /.*staff_.*\.spec\.js/,
dependencies: ['门店员工鉴权'],
},
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});

View File

@ -0,0 +1,14 @@
import { faker } from '@faker-js/faker';
// 参考文档
// https://fakerjs.dev/guide/
// 生成手机号
const randomPhoneNumber = faker.helpers.fromRegExp(/1[3-9][0-9]{8}/);
console.log(randomPhoneNumber);
// 生成名字
const randomName = faker.person.fullName();
console.log(randomName);
// 生成城市
const randomCity = faker.location.city();
console.log(randomCity);

View File

@ -0,0 +1,449 @@
// @ts-check
const { test, expect } = require('@playwright/test');
test.beforeEach(async ({ page }) => {
await page.goto('https://demo.playwright.dev/todomvc');
});
const TODO_ITEMS = [
'buy some cheese',
'feed the cat',
'book a doctors appointment'
];
test.describe('New Todo', () => {
test('should allow me to add todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create 1st todo.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Make sure the list only has one todo item.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0]
]);
// Create 2nd todo.
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
// Make sure the list now has two todo items.
await expect(page.getByTestId('todo-title')).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[1]
]);
await checkNumberOfTodosInLocalStorage(page, 2);
});
test('should clear text input field when an item is added', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create one todo item.
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
// Check that input is empty.
await expect(newTodo).toBeEmpty();
await checkNumberOfTodosInLocalStorage(page, 1);
});
test('should append new items to the bottom of the list', async ({ page }) => {
// Create 3 items.
await createDefaultTodos(page);
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
// Check test using different methods.
await expect(page.getByText('3 items left')).toBeVisible();
await expect(todoCount).toHaveText('3 items left');
await expect(todoCount).toContainText('3');
await expect(todoCount).toHaveText(/3/);
// Check all items in one call.
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
await checkNumberOfTodosInLocalStorage(page, 3);
});
});
test.describe('Mark all as completed', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test.afterEach(async ({ page }) => {
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should allow me to mark all items as completed', async ({ page }) => {
// Complete all todos.
await page.getByLabel('Mark all as complete').check();
// Ensure all todos have 'completed' class.
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
});
test('should allow me to clear the complete state of all items', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
// Check and then immediately uncheck.
await toggleAll.check();
await toggleAll.uncheck();
// Should be no completed classes.
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
});
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
const toggleAll = page.getByLabel('Mark all as complete');
await toggleAll.check();
await expect(toggleAll).toBeChecked();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Uncheck first todo.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').uncheck();
// Reuse toggleAll locator and make sure its not checked.
await expect(toggleAll).not.toBeChecked();
await firstTodo.getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
// Assert the toggle all is checked again.
await expect(toggleAll).toBeChecked();
});
});
test.describe('Item', () => {
test('should allow me to mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
// Check first item.
const firstTodo = page.getByTestId('todo-item').nth(0);
await firstTodo.getByRole('checkbox').check();
await expect(firstTodo).toHaveClass('completed');
// Check second item.
const secondTodo = page.getByTestId('todo-item').nth(1);
await expect(secondTodo).not.toHaveClass('completed');
await secondTodo.getByRole('checkbox').check();
// Assert completed class.
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).toHaveClass('completed');
});
test('should allow me to un-mark items as complete', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// Create two items.
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const firstTodo = page.getByTestId('todo-item').nth(0);
const secondTodo = page.getByTestId('todo-item').nth(1);
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
await firstTodoCheckbox.check();
await expect(firstTodo).toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await firstTodoCheckbox.uncheck();
await expect(firstTodo).not.toHaveClass('completed');
await expect(secondTodo).not.toHaveClass('completed');
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
});
test('should allow me to edit an item', async ({ page }) => {
await createDefaultTodos(page);
const todoItems = page.getByTestId('todo-item');
const secondTodo = todoItems.nth(1);
await secondTodo.dblclick();
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
// Explicitly assert the new text value.
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2]
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
});
test.describe('Editing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should hide other controls when editing', async ({ page }) => {
const todoItem = page.getByTestId('todo-item').nth(1);
await todoItem.dblclick();
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
await expect(todoItem.locator('label', {
hasText: TODO_ITEMS[1],
})).not.toBeVisible();
await checkNumberOfTodosInLocalStorage(page, 3);
});
test('should save edits on blur', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should trim entered text', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
'buy some sausages',
TODO_ITEMS[2],
]);
await checkTodosInLocalStorage(page, 'buy some sausages');
});
test('should remove the item if an empty text string was entered', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
await expect(todoItems).toHaveText([
TODO_ITEMS[0],
TODO_ITEMS[2],
]);
});
test('should cancel edits on escape', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).dblclick();
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
await expect(todoItems).toHaveText(TODO_ITEMS);
});
});
test.describe('Counter', () => {
test('should display the current number of todo items', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
// create a todo count locator
const todoCount = page.getByTestId('todo-count')
await newTodo.fill(TODO_ITEMS[0]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('1');
await newTodo.fill(TODO_ITEMS[1]);
await newTodo.press('Enter');
await expect(todoCount).toContainText('2');
await checkNumberOfTodosInLocalStorage(page, 2);
});
});
test.describe('Clear completed button', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
});
test('should display the correct text', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
});
test('should remove completed items when clicked', async ({ page }) => {
const todoItems = page.getByTestId('todo-item');
await todoItems.nth(1).getByRole('checkbox').check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(todoItems).toHaveCount(2);
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should be hidden when there are no items that are completed', async ({ page }) => {
await page.locator('.todo-list li .toggle').first().check();
await page.getByRole('button', { name: 'Clear completed' }).click();
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
});
});
test.describe('Persistence', () => {
test('should persist its data', async ({ page }) => {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS.slice(0, 2)) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
const todoItems = page.getByTestId('todo-item');
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
await firstTodoCheck.check();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
// Ensure there is 1 completed item.
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
// Now reload.
await page.reload();
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
await expect(firstTodoCheck).toBeChecked();
await expect(todoItems).toHaveClass(['completed', '']);
});
});
test.describe('Routing', () => {
test.beforeEach(async ({ page }) => {
await createDefaultTodos(page);
// make sure the app had a chance to save updated todos in storage
// before navigating to a new view, otherwise the items can get lost :(
// in some frameworks like Durandal
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
});
test('should allow me to display active items', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await expect(todoItem).toHaveCount(2);
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
});
test('should respect the back button', async ({ page }) => {
const todoItem = page.getByTestId('todo-item');
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await test.step('Showing all items', async () => {
await page.getByRole('link', { name: 'All' }).click();
await expect(todoItem).toHaveCount(3);
});
await test.step('Showing active items', async () => {
await page.getByRole('link', { name: 'Active' }).click();
});
await test.step('Showing completed items', async () => {
await page.getByRole('link', { name: 'Completed' }).click();
});
await expect(todoItem).toHaveCount(1);
await page.goBack();
await expect(todoItem).toHaveCount(2);
await page.goBack();
await expect(todoItem).toHaveCount(3);
});
test('should allow me to display completed items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Completed' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(1);
});
test('should allow me to display all items', async ({ page }) => {
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
await page.getByRole('link', { name: 'Active' }).click();
await page.getByRole('link', { name: 'Completed' }).click();
await page.getByRole('link', { name: 'All' }).click();
await expect(page.getByTestId('todo-item')).toHaveCount(3);
});
test('should highlight the currently applied filter', async ({ page }) => {
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
//create locators for active and completed links
const activeLink = page.getByRole('link', { name: 'Active' });
const completedLink = page.getByRole('link', { name: 'Completed' });
await activeLink.click();
// Page change - active items.
await expect(activeLink).toHaveClass('selected');
await completedLink.click();
// Page change - completed items.
await expect(completedLink).toHaveClass('selected');
});
});
async function createDefaultTodos(page) {
// create a new todo locator
const newTodo = page.getByPlaceholder('What needs to be done?');
for (const item of TODO_ITEMS) {
await newTodo.fill(item);
await newTodo.press('Enter');
}
}
/**
* @param {import('@playwright/test').Page} page
* @param {number} expected
*/
async function checkNumberOfTodosInLocalStorage(page, expected) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).length === e;
}, expected);
}
/**
* @param {import('@playwright/test').Page} page
* @param {number} expected
*/
async function checkNumberOfCompletedTodosInLocalStorage(page, expected) {
return await page.waitForFunction(e => {
return JSON.parse(localStorage['react-todos']).filter(i => i.completed).length === e;
}, expected);
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} title
*/
async function checkTodosInLocalStorage(page, title) {
return await page.waitForFunction(t => {
return JSON.parse(localStorage['react-todos']).map(i => i.title).includes(t);
}, title);
}

15
tests/fixtures/appointmentFixture.ts vendored Normal file
View File

@ -0,0 +1,15 @@
import { test as base } from '@playwright/test';
import { AppointmentPage } from '@/pages/appointmentPage';
type MyFixture = {
appointmentPage: AppointmentPage;
};
export const test = base.extend<MyFixture>({
appointmentPage: async ({ page }, use) => {
const appointmentPage = new AppointmentPage(page);
await use(appointmentPage);
},
});
export { expect } from '@playwright/test';

81
tests/fixtures/baseFixture.ts vendored Normal file
View File

@ -0,0 +1,81 @@
import { test as base, expect } from '@playwright/test';
import { HomeNavigation } from '@/pages/homeNavigationPage';
import { writeIndexedDB } from '@/utils/indexedDBUtils';
import { readFileSync } from 'fs';
type MyFixture = {
homeNavigation: HomeNavigation;
billSet: Set<string>;
};
export const test = base.extend<MyFixture>({
page: async ({ page, baseURL }, use) => {
await page.addLocatorHandler(page.locator('.left_img > .close_btn > .close_icon > svg'), async () => {
await page.locator('.left_img > .close_btn > .close_icon > svg').click();
await expect(page.locator('.versionModal_main_content')).toBeVisible();
});
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');
});
if (!mobileObject && mobileObject !== 'null') {
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');
}
await page.evaluate(
async ({ fnString, jsonData }) => {
// 在浏览器上下文中动态创建函数
const writeIndexedDBFn = new Function('return ' + fnString)();
// 调用函数并传递参数
return await writeIndexedDBFn(jsonData);
},
{ fnString: writeIndexedDB.toString(), jsonData },
);
// 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 秒
);
await page.getByRole('button', { name: /开\s单/ }).waitFor();
await use(page);
},
/**
*
*/
homeNavigation: async ({ page }, use) => {
const homeNavigation = new HomeNavigation(page);
await use(homeNavigation);
},
/**
*
*/
billSet: async ({ page }, use) => {
const billSet = new Set<string>();
await use(billSet);
},
});
export { expect } from '@playwright/test';

28
tests/fixtures/boss_common.ts vendored Normal file
View File

@ -0,0 +1,28 @@
import { mergeTests, mergeExpects } from '@playwright/test';
import { test as cashierTest } from '@/fixtures/cashierFixture';
import { test as appointmentTest } from '@/fixtures/appointmentFixture';
import { test as wasteBookTest } from '@/fixtures/wasteBookFixture';
import { test as customerTest } from '@/fixtures/customerFixture';
import { test as inventoryTest } from '@/fixtures/inventoryFixture';
import { test as goalTest } from '@/fixtures/goalFixture';
import { test as reportTest, expect as reportExpect } from '@/fixtures/reportFixture';
import { test as marketingTest } from '@/fixtures/marketingFixture';
import { test as baseTest } from '@/fixtures/baseFixture';
import { test as tableTest } from '@/fixtures/tableFixture';
import { test as componentsTest } from '@/fixtures/componentsFixture';
export const test = mergeTests(
cashierTest,
appointmentTest,
customerTest,
wasteBookTest,
inventoryTest,
goalTest,
reportTest,
baseTest,
marketingTest,
tableTest,
componentsTest,
);
export const expect = mergeExpects(reportExpect);

15
tests/fixtures/cashierFixture.ts vendored Normal file
View File

@ -0,0 +1,15 @@
import { test as base } from '@playwright/test';
import { CashierRoomPage } from '@/pages/cashierRoomPage.js';
type MyFixture = {
cashierRoomPage: CashierRoomPage;
};
export const test = base.extend<MyFixture>({
cashierRoomPage: async ({ page }, use) => {
const cashierRoomPage = new CashierRoomPage(page);
await use(cashierRoomPage);
},
});
export { expect } from '@playwright/test';

13
tests/fixtures/componentsFixture.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import { test as base } from '@playwright/test';
import { NumberInput } from '@/pages/components';
type MyFixture = {
numberInput: NumberInput;
};
export const test = base.extend<MyFixture>({
numberInput: async ({ page }, use) => {
const numberInput = new NumberInput(page);
await use(numberInput);
},
});

148
tests/fixtures/customerFixture.ts vendored Normal file
View File

@ -0,0 +1,148 @@
import { test as base } from '@playwright/test';
import { CustomerPage, CustomerDetailsPage, CustomerAnalysisPage } from '@/pages/customer';
import { Customer } from '@/utils/customer';
type MyFixture = {
singleCustomerPage: {
createCustomer: (customer: Customer) => Promise<void>;
setInvalidCustomer: (customer: Customer) => Promise<void>;
};
moreCustomerPage: {
createMoreCustomer: (customer: Customer[]) => Promise<void>;
setMoreInvalidCustomer: (customer: Customer[]) => Promise<void>;
};
customerPage: CustomerPage;
customerDetailsPage: CustomerDetailsPage;
customerAnalysisPage: CustomerAnalysisPage;
createCustomer: Customer;
createCustomers: (customerNumber: number) => Promise<Customer[]>;
createCustomCustomer: (customer: Customer) => Promise<void>;
createCustomCustomers: (customers: Customer[]) => Promise<void>;
};
export const test = base.extend<MyFixture>({
/**
*
*/
createCustomer: async ({ page, baseURL }, use) => {
if (!baseURL) {
throw new Error('baseUrl is required');
}
const customerPage = new CustomerPage(page);
const customer = new Customer(1, 1);
await customerPage.createCustomer(customer);
await page.goto(baseURL);
await use(customer);
await page.goto(baseURL);
await page.getByRole('button', { name: /开\s单/ }).waitFor();
await page.goto('/#/member/member-schame');
await customerPage.setInvalidCustomer(customer);
},
/**
*
*/
createCustomers: async ({ page, baseURL }, use): Promise<void> => {
if (!baseURL) {
throw new Error('baseUrl is required');
}
const customerPage = new CustomerPage(page);
let createdCustomers: Customer[] = [];
/**
*
* @param customerNumber { number }
* @returns { Promise<Customer[]> }
*/
const createCustomers = async (customerNumber: number): Promise<Customer[]> => {
for (let i = 0; i < customerNumber; i++) {
const customer = new Customer(1, 1);
await customerPage.createCustomer(customer);
createdCustomers.push(customer);
}
return createdCustomers;
};
await use(createCustomers);
if (createdCustomers.length === 0) return;
// 测试结束后将客户设置为无效
await page.goto(baseURL);
await page.getByRole('button', { name: /开\s单/ }).waitFor();
await page.goto('/#/member/member-schame');
await customerPage.setMoreInvalidCustomer(createdCustomers);
},
/**
*
*/
createCustomCustomer: async ({ page, baseURL }, use) => {
if (!baseURL) {
throw new Error('baseUrl is required');
}
const customerPage = new CustomerPage(page);
let tmp: Customer | undefined;
const setCustomer = async (customer: Customer) => {
await customerPage.createCustomer(customer);
tmp = customer;
};
await use(setCustomer);
if (!tmp) return;
await page.goto(baseURL);
await page.getByRole('button', { name: /开\s单/ }).waitFor();
await page.goto('/#/member/member-schame');
await customerPage.setInvalidCustomer(tmp);
},
/**
*
*/
createCustomCustomers: async ({ page, baseURL }, use) => {
if (!baseURL) {
throw new Error('baseUrl is required');
}
const customerPage = new CustomerPage(page);
let tmp: Customer[] | undefined;
const setCustomers = async (customers: Customer[]) => {
await customerPage.createMoreCustomer(customers);
tmp = customers;
};
await use(setCustomers);
if (!tmp) return;
await page.goto(baseURL);
await page.getByRole('button', { name: /开\s单/ }).waitFor();
await page.goto('/#/member/member-schame');
await customerPage.setMoreInvalidCustomer(tmp);
},
/**
*
*/
customerPage: async ({ page }, use) => {
const customerPage = new CustomerPage(page);
await use(customerPage);
},
/**
*
*/
customerDetailsPage: async ({ page }, use) => {
const customerDetailsPage = new CustomerDetailsPage(page);
await use(customerDetailsPage);
},
/**
*
*/
customerAnalysisPage: async ({ page }, use) => {
const customerAnalysisPage = new CustomerAnalysisPage(page);
await use(customerAnalysisPage);
},
});

13
tests/fixtures/goalFixture.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import { test as base } from '@playwright/test';
import { GoalPage } from '@/pages/goalPage';
type MyFixture = { goalPage: GoalPage };
export const test = base.extend<MyFixture>({
goalPage: async ({ page }, use) => {
const goalPage = new GoalPage(page);
await use(goalPage);
},
});
export { expect } from '@playwright/test';

21
tests/fixtures/inventoryFixture.ts vendored Normal file
View File

@ -0,0 +1,21 @@
import { test as base } from '@playwright/test';
import { TransferManagementPage, InventoryManagementPage } from '@/pages/inventory';
type MyFixture = {
transferManagementPage: TransferManagementPage;
inventoryManagementPage: InventoryManagementPage;
};
export const test = base.extend<MyFixture>({
transferManagementPage: async ({ page }, use) => {
const transferManagementPage = new TransferManagementPage(page);
await use(transferManagementPage);
},
inventoryManagementPage: async ({ page }, use) => {
const inventoryManagementPage = new InventoryManagementPage(page);
await use(inventoryManagementPage);
},
});
export { expect } from '@playwright/test';

18
tests/fixtures/marketingFixture.ts vendored Normal file
View File

@ -0,0 +1,18 @@
import { test as base } from '@playwright/test';
import { MarketingPage, MarketingInviteGuestsPage } from '@/pages/marketing';
type MyFixture = {
marketingPage: MarketingPage;
marketingInviteGuestsPage: MarketingInviteGuestsPage;
};
export const test = base.extend<MyFixture>({
marketingPage: async ({ page }, use) => {
const marketingPage = new MarketingPage(page);
await use(marketingPage);
},
marketingInviteGuestsPage: async ({ page }, use) => {
const marketingInviteGuestsPage = new MarketingInviteGuestsPage(page);
await use(marketingInviteGuestsPage);
},
});

52
tests/fixtures/merchantFixture.ts vendored Normal file
View File

@ -0,0 +1,52 @@
import { test as base } from '@/fixtures/boss_common';
import { BrowserContext, Page, TestInfo } from '@playwright/test';
import path from 'path';
import fs from 'fs';
// 定义商户数据类型
type Merchant = {
id: string;
name: string;
storageState: string;
};
// 二维码能够解析出url
const authDirPath = path.resolve(__dirname, '../../.auth');
if (!fs.existsSync(authDirPath)) {
fs.mkdirSync(authDirPath, { recursive: true });
}
const merchants: Merchant[] = [
{
id: '14920',
name: '商户 1',
storageState: path.resolve(authDirPath, 'admin_first.json'),
},
{
id: '14983',
name: '商户 2',
storageState: path.resolve(authDirPath, 'admin_second.json'),
},
];
export const test = base.extend<{
page: Page;
}>({
page: async ({ browser, baseURL }, use, workerInfo: TestInfo) => {
// 根据 workerIndex 从商户池中获取对应商户
const selectedMerchant = merchants[workerInfo.workerIndex % merchants.length];
// const selectedMerchant = merchants[1];
const context: BrowserContext = await browser.newContext({
storageState: selectedMerchant.storageState,
});
const page: Page = await context.newPage();
console.log(`使用的商户: ${selectedMerchant.name}`);
await page.goto(baseURL || '');
// 使用商户信息进行测试
await use(page);
await page.close();
await context.close();
},
});
export { expect } from './boss_common';

66
tests/fixtures/reportFixture.ts vendored Normal file
View File

@ -0,0 +1,66 @@
import { test as base, expect as baseExpect } from '@playwright/test';
import {
ReportPage,
PerformanceSummaryReportPage,
SpendingSummaryReportPage,
PerformanceDetailReportPage,
CardBalanceChangeReportPage,
ItemSalesConsumptionAccessReportPage,
SalesCostSummaryReportPage,
CustomerConsumptionAnalysisReportPage,
} from '@/pages/report';
type MyFixture = {
reportPage: ReportPage;
performanceSummaryReportPage: PerformanceSummaryReportPage;
spendingSummaryReportPage: SpendingSummaryReportPage;
performanceDetailReportPage: PerformanceDetailReportPage;
cardBalanceChangeReportPage: CardBalanceChangeReportPage;
itemSalesConsumptionAccessReportPage: ItemSalesConsumptionAccessReportPage;
salesCostSummaryReportPage: SalesCostSummaryReportPage;
customerConsumptionAnalysisReportPage: CustomerConsumptionAnalysisReportPage;
};
export const test = base.extend<MyFixture>({
reportPage: async ({ page }, use) => {
const reportPage = new ReportPage(page);
await use(reportPage);
},
performanceSummaryReportPage: async ({ page }, use) => {
const performanceSummaryReportPage = new PerformanceSummaryReportPage(page);
await use(performanceSummaryReportPage);
},
performanceDetailReportPage: async ({ page }, use) => {
const performanceDetailReportPage = new PerformanceDetailReportPage(page);
await use(performanceDetailReportPage);
},
cardBalanceChangeReportPage: async ({ page }, use) => {
const cardBalanceChangeReportPage = new CardBalanceChangeReportPage(page);
await use(cardBalanceChangeReportPage);
},
spendingSummaryReportPage: async ({ page }, use) => {
const spendingSummaryReportPage = new SpendingSummaryReportPage(page);
await use(spendingSummaryReportPage);
},
itemSalesConsumptionAccessReportPage: async ({ page }, use) => {
const itemSalesConsumptionAccessReportPage = new ItemSalesConsumptionAccessReportPage(page);
await use(itemSalesConsumptionAccessReportPage);
},
salesCostSummaryReportPage: async ({ page }, use) => {
const salesCostSummaryReportPage = new SalesCostSummaryReportPage(page);
await use(salesCostSummaryReportPage);
},
customerConsumptionAnalysisReportPage: async ({ page }, use) => {
const customerConsumptionAnalysisReportPage = new CustomerConsumptionAnalysisReportPage(page);
await use(customerConsumptionAnalysisReportPage);
},
});
export const expect = baseExpect.extend({});

214
tests/fixtures/staff.js vendored Normal file
View File

@ -0,0 +1,214 @@
//@ts-check
// 默认为生产
let nodeEnv = process.env.NODE_ENV || "production";
nodeEnv = nodeEnv === "staging" ? "production" : nodeEnv;
/**
* - 员工数据 门店 部门 员工 姓名
* - staffData.firstStore.firstSector.employee_1.name
* - 员工数据 门店 部门 员工 手机号
* - staffData.firstStore.firstSector.employee_1.phone
* - 员工数据 门店 部门 员工 ID
* - staffData.firstStore.firstSector.employee_1.id
*/
let staffData = {
firstStore: {
firstSector: {
name: "美容部",
employee_1: {
name: "张伟",
phone: "13812345678",
id: { production: 3, test: 1 },
},
employee_2: {
name: "李娜",
phone: "13987654321",
id: { production: 4, test: 2 },
},
employee_3: {
name: "王芳",
phone: "13723456789",
id: { production: 5, test: 3 },
},
employee_4: {
name: "陈刚",
phone: "13698765432",
id: { production: 6, test: 4 },
},
employee_5: {
name: "赵军",
phone: "13512349876",
id: { production: 7, test: 5 },
},
employee_6: {
name: "刘强",
phone: "13498761234",
id: { production: 8, test: 6 },
},
employee_7: {
name: "周萍",
phone: "13365432109",
id: { production: 9, test: 7 },
},
employee_8: {
name: "吴浩",
phone: "13287654329",
id: { production: 10, test: 8 },
},
employee_9: {
name: "徐亮",
phone: "13123459876",
id: { production: 11, test: 9 },
},
employee_10: {
name: "杨雪",
phone: "13098761234",
id: { production: 12, test: 10 },
},
},
secondSector: {
name: "医美部",
employee_1: {
name: "赵伟",
phone: "13923456789",
id: { production: 13, test: 11 },
},
employee_2: {
name: "钱丽",
phone: "13898765432",
id: { production: 14, test: 12 },
},
employee_3: {
name: "孙峰",
phone: "13712349876",
id: { production: 15, test: 13 },
},
employee_4: {
name: "李涛",
phone: "13687654321",
id: { production: 16, test: 14 },
},
employee_5: {
name: "周慧",
phone: "13598761234",
id: { production: 17, test: 15 },
},
employee_6: {
name: "吴凯",
phone: "13465432109",
id: { production: 18, test: 16 },
},
employee_7: {
name: "郑翔",
phone: "13387654329",
id: { production: 19, test: 17 },
},
employee_8: {
name: "冯敏",
phone: "13223459876",
id: { production: 20, test: 18 },
},
employee_9: {
name: "朱强",
phone: "13198761234",
id: { production: 21, test: 19 },
},
employee_10: {
name: "何平",
phone: "13065432198",
id: { production: 22, test: 20 },
},
},
},
secondStore: {
name: "美容部",
firstSector: {
employee_1: {
name: "张凯",
phone: "13865432198",
id: { production: 1, test: 1 },
},
employee_2: {
name: "李军",
phone: "13923459876",
id: { production: 2, test: 2 },
},
employee_3: {
name: "王涛",
phone: "13798761234",
id: { production: 3, test: 3 },
},
employee_4: {
name: "陈敏",
phone: "13654321987",
id: { production: 4, test: 4 },
},
employee_5: {
name: "赵峰",
phone: "13523456789",
id: { production: 5, test: 5 },
},
employee_6: {
name: "刘丽",
phone: "13487654321",
id: { production: 6, test: 6 },
},
employee_7: {
name: "周亮",
phone: "13398765432",
id: { production: 7, test: 7 },
},
employee_8: {
name: "吴平",
phone: "13212349876",
id: { production: 8, test: 8 },
},
employee_9: {
name: "徐浩",
phone: "13165432109",
id: { production: 9, test: 9 },
},
employee_10: {
name: "孙杰",
phone: "13087654329",
id: { production: 10, test: 10 },
},
},
},
};
/**
*
* @param {Object} staffData
* @param {string} nodeEnv
* @returns
*/
function init(staffData, nodeEnv) {
const updatedData = JSON.parse(JSON.stringify(staffData)); // 深拷贝对象,避免修改原数据
function updateIds(sector) {
Object.keys(sector).forEach(key => {
const employee = sector[key];
if (employee && employee.id && employee.id[nodeEnv] !== undefined) {
employee.id = employee.id[nodeEnv]; // 将 id 替换为 production 或 test 的值
}
});
}
// 遍历 firstStore 和 secondStore 下的各个 sector更新员工的 id
Object.keys(updatedData).forEach(storeKey => {
const store = updatedData[storeKey];
Object.keys(store).forEach(sectorKey => {
const sector = store[sectorKey];
if (typeof sector === "object" && sector !== null && sector.name) {
updateIds(sector); // 更新每个 sector 的员工 id
}
});
});
return updatedData; // 返回更新后的数据
}
staffData = init(staffData, nodeEnv);
export { staffData };

46
tests/fixtures/staffFixture.ts vendored Normal file
View File

@ -0,0 +1,46 @@
import { test as base } from '@playwright/test';
import { HomeNavigation } from '@/pages/homeNavigationPage.js';
import { TransferManagementPage } from '@/pages/inventory';
export const test = base.extend({
/**
* @type { import("@playwright/test").Page }
*/
firstStaffPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: '.auth/user_1.json',
});
const page = await context.newPage();
await page.goto(process.env.BASE_URL ?? '');
await use(page);
await context.close();
},
/**
* @type { import("@playwright/test").Page }
*/
secondStaffPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: '.auth/user_2.json',
});
const page = await context.newPage();
await page.goto(process.env.BASE_URL ?? '');
await use(page);
await context.close();
},
/**
* @type { HomeNavigation }
*/
staffHomeNavigation: async ({ firstStaffPage }, use) => {
const homeNavigation = new HomeNavigation(firstStaffPage);
await use(homeNavigation);
},
/**
* @type { TransferManagementPage }
*/
transferManagementPage: async ({ firstStaffPage }, use) => {
const transferManagementPage = new TransferManagementPage(firstStaffPage);
await use(transferManagementPage);
},
});
export { expect } from '@playwright/test';

11
tests/fixtures/staff_common.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import { mergeTests } from '@playwright/test';
import { test as cashierTest } from '@/fixtures/cashierFixture';
import { test as appointmentTest } from '@/fixtures/appointmentFixture';
import { test as inventoryTest } from '@/fixtures/inventoryFixture';
import { test as staffTest } from '@/fixtures/staffFixture';
import { test as baseTest } from '@/fixtures/baseFixture';
import { test as componentsTest } from '@/fixtures/componentsFixture';
export const test = mergeTests(cashierTest, appointmentTest, inventoryTest, staffTest, baseTest, componentsTest);
export { expect } from '@playwright/test';

9
tests/fixtures/tableFixture.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import { test as base } from '@playwright/test';
import { TablePage } from '@/pages/tablePage';
export const test = base.extend<{ tablePage: TablePage }>({
tablePage: async ({ page }, use) => {
const tablePage = new TablePage(page);
await use(tablePage);
},
});

1020
tests/fixtures/userconfig.js vendored Normal file

File diff suppressed because it is too large Load Diff

14
tests/fixtures/wasteBookFixture.ts vendored Normal file
View File

@ -0,0 +1,14 @@
import { test as base } from '@playwright/test';
import { WasteBookBusinessRecordPage } from '@/pages/wastebook';
type MyFixture = { wasteBookBusinessRecordPage: WasteBookBusinessRecordPage };
export const test = base.extend<MyFixture>({
/**
* @type { WasteBookBusinessRecordPage }
*/
wasteBookBusinessRecordPage: async ({ page }, use) => {
const wasteBookBusinessRecordPage = new WasteBookBusinessRecordPage(page);
await use(wasteBookBusinessRecordPage);
},
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

BIN
tests/imgs/upload.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
tests/imgs/商品M.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -0,0 +1,318 @@
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();
};
}

View File

@ -0,0 +1,39 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { type Customer } from '@/utils/customer';
export class CashierRoomPage {
page: Page;
$userInfo: Locator;
statusCss: {};
/**
* -
*/
constructor(page: Page) {
this.page = page;
this.$userInfo = this.page.locator('.a_userInfo');
this.statusCss = {
doing: 'doing',
wait: 'wait',
pass: 'pass',
};
}
/**
*
* @param {import("../utils/customer").Customer} customer
* @param {string} expectedStatus
* - 'doing'
* - 'wait'
* - 'pass'
*/
checkStatus = async (customer: Customer, expectedStatus: string) => {
const userInfo = this.$userInfo.filter({ hasText: customer.username });
const count = await userInfo.count();
if (count !== 1) {
throw new Error(`顾客${JSON.stringify(customer)}${count}个预约房间`);
}
await expect(userInfo).toHaveClass(new RegExp(`${this.statusCss[expectedStatus]}`));
};
}

View File

@ -0,0 +1,3 @@
import { NumberInput } from './numberInput';
export { NumberInput };

View File

@ -0,0 +1,86 @@
import { Locator, Page } from '@playwright/test';
export class NumberInput {
private readonly page: Page;
private readonly popupLocator: Locator;
private readonly inputLocator: Locator;
private readonly confirmButtonLocator: Locator;
private readonly delButtonLocator: Locator;
private readonly delAllButtonLocator: Locator;
private readonly pointInputLocator: Locator;
private readonly commonInputLocator: Locator;
/**
*
* @param page
*/
constructor(page: Page) {
this.page = page;
this.popupLocator = this.page.locator('div.popup_content');
this.commonInputLocator = this.popupLocator.getByPlaceholder('');
this.inputLocator = this.popupLocator.getByPlaceholder('请输入内容');
this.pointInputLocator = this.popupLocator.getByPlaceholder('请输入积分');
this.confirmButtonLocator = this.popupLocator.locator('button.sure');
this.delButtonLocator = this.popupLocator.locator('button.del');
this.delAllButtonLocator = this.popupLocator.locator('button.delAll');
}
/**
*
*/
async setCommonValue(value: number): Promise<void> {
await this.commonInputLocator.fill(value.toString());
}
/**
*
* @param value
*/
async setValue(value: number): Promise<void> {
await this.inputLocator.fill(value.toString());
}
/**
*
*/
async setString(value: string): Promise<void> {
await this.inputLocator.fill(value);
}
/**
*
* @param value
*/
async setPointValue(value: number): Promise<void> {
await this.pointInputLocator.fill(value.toString());
}
/**
*
* @param value
*/
async setInputValue(value: number): Promise<void> {
await this.page.getByRole('button', { name: value.toString() }).click();
}
/**
*
*/
async confirmValue(): Promise<void> {
await this.confirmButtonLocator.click();
}
/**
*
*/
async delValue(): Promise<void> {
await this.delButtonLocator.click();
}
/**
*
*/
async delAllValue(): Promise<void> {
await this.delAllButtonLocator.click();
}
}

View File

@ -0,0 +1,36 @@
import { Page } from 'playwright';
export class CustomerAnalysisPage {
page: Page;
subPages: { name: string; url?: string[] }[];
/**
*
* @param {import("@playwright/test").Page} page
*/
constructor(page: Page) {
this.page = page;
this.subPages = [{ name: '项目余量分析' }, { name: '套餐消耗升单分析' }, { name: '顾客项目分析' }];
}
/**
*
* @param {string} subPageName
* -
* -
* -
*/
gotoSubPage = async (subPageName: string) => {
if (!this.subPages.some(({ name }) => name === subPageName)) {
throw new Error(`${subPageName} is not a valid sub page name`);
}
const $dropDown = this.page
.locator('.top_tab .tab_item', {
hasText: '顾客分析',
})
.locator('.ant-dropdown-link');
await $dropDown.click();
await this.page.getByRole('menuitem', { name: subPageName }).click();
};
}

View File

@ -0,0 +1,31 @@
import { expect, Page } from '@playwright/test';
export class CustomerDetailsPage {
page: Page;
subPages: { name: string; url?: string[] }[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this.subPages = [{ name: '基本资料' }, { name: '流水' }, { name: '动态' }, { name: '日志' }];
}
/**
*
* @param subPageName
* -
* -
* -
* -
*/
gotoSubPage = async (subPageName: string) => {
if (!this.subPages.findIndex(e => e.name === subPageName)) {
throw new Error(`${subPageName} is not in the subPages list`);
}
const $subPage = this.page.getByRole('tab', { name: subPageName });
await $subPage.click();
await expect($subPage).toHaveClass(/active/);
await this.page.waitForLoadState();
};
}

View File

@ -0,0 +1,379 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { HomeNavigation } from '../homeNavigationPage.js';
import { Customer, employee } from '../../utils/customer';
import { waitSpecifyApiLoad } from '../../utils/utils.js';
type SubPage = {
name: string;
url: string[];
};
/**
* `CustomerPage`
*
*/
export class CustomerPage {
page: Page;
private readonly homeNavigation: HomeNavigation;
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 source: string[] = [
'邀客',
'员工带客',
'美团',
'大众点评',
'客带客',
'上门客人',
'百度糯米',
'支付宝',
];
$register: Locator;
$tabItem: Locator;
$searchInput: Locator;
$searchConfirm: 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.$searchConfirm = this.page.getByText('搜索', { exact: true });
this.homeNavigation = new HomeNavigation(this.page);
}
/**
*
* @param {string} subPageName
* -
* -
* -
* -
* -
*/
gotoSubPage = async (subPageName: string) => {
const subPage = this.subPages.find(e => e.name === subPageName);
if (!subPage) {
throw new Error(`子页面 ${subPageName} 不存在`);
}
const $subPage = this.$tabItem.filter({ hasText: subPageName });
await $subPage.waitFor();
const classAttribute = await $subPage.getAttribute('class', { timeout: 5000 });
if (classAttribute && classAttribute.includes('active')) {
return;
}
await Promise.all([
expect(async () => {
await $subPage.click();
await expect($subPage).toHaveClass(/active/);
}).toPass(),
waitSpecifyApiLoad(this.page, subPage.url),
]);
};
/**
*
* @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.$searchConfirm.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 closeButton = this.page.locator('.member_info_box .close_icons > svg');
await closeButton.click();
await closeButton.waitFor({ state: 'detached' });
};
/**
*
* @param {Customer} customer
*/
createCustomer = async (customer: Customer) => {
const customerPage = '/#/member/member-schame';
await this.page.goto(customerPage);
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) => {
if (gender === 0) {
await this.$register.locator('label').filter({ hasText: '女性' }).click();
} else if (gender === 1) {
await this.$register.locator('label').filter({ hasText: '男性' }).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) {
const { year, month, day } = birthday;
const birthdayLocator = this.$register.locator('.ant-form-item', { hasText: '生日' });
if (year) {
await birthdayLocator.getByText('年份').click();
await this.page.getByRole('option', { name: `${year}` }).click();
await expect(this.page.getByRole('option', { name: `${year}` })).not.toBeVisible();
}
if (month && day) {
await birthdayLocator.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.source[source]).click();
};
/**
*
* @param {string} remark
*/
private readonly fillRemark = async (remark: string) => {
if (remark) {
await this.$register.getByPlaceholder('请输入1-100个字符备注内容').fill(remark);
}
};
/**
*
* @param {string} phone
*/
private readonly confirmCreation = async (phone: string) => {
const [response] = await Promise.all([
this.page.waitForResponse(
async res => res.url().includes('/invalid_check') && (await res.json()).code === 'SUCCESS',
),
this.page.getByRole('button', { name: '确认创建' }).click(),
]);
const responseBody = await response.json();
const phoneStatus = responseBody?.content?.status;
if (!phoneStatus) {
return;
}
await this.page.getByText('系统查询到当前手机号被建档后转为无效客,是否要恢复无效客?').waitFor();
await this.page.getByRole('button', { name: '重新建档' }).click();
const popupWindow = this.page.locator('.ant-message');
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').waitFor();
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) {
this.page.reload();
await this.setInvalidCustomer(customer);
}
};
}

View File

@ -0,0 +1,5 @@
import { CustomerAnalysisPage } from './customerAnalysisPage';
import { CustomerDetailsPage } from './customerDetailsPage';
import { CustomerPage } from './customerPage';
export { CustomerAnalysisPage, CustomerDetailsPage, CustomerPage };

33
tests/pages/goalPage.ts Normal file
View File

@ -0,0 +1,33 @@
import { Page } from '@playwright/test';
import { waitSpecifyApiLoad } from '@/utils/utils.js';
export class GoalPage {
readonly page: Page;
subPages: { name: string; url?: string[] }[];
constructor(page: Page) {
this.page = page;
this.subPages = [{ name: '员工目标' }, { name: '精细化管理', url: ['/user_kpi_detail'] }];
}
/**
*
* @param subPageName - |
*/
async gotoSubPage(subPageName: string) {
if (!this.subPages.some(({ name }) => name === subPageName)) {
throw new Error(`${subPageName} is not a valid sub page name`);
}
const $topTab = this.page.locator('.top_tab .tab_item').filter({ hasText: subPageName });
if ((await $topTab.getAttribute('class', { timeout: 500 }))?.includes('active')) {
return;
}
await Promise.all([
$topTab.click(),
waitSpecifyApiLoad(this.page, this.subPages.find(({ name }) => name === subPageName)?.url),
]);
}
}

View File

@ -0,0 +1,63 @@
import { expect, type Locator, type Page } from '@playwright/test';
export class HomeNavigation {
page: Page;
$$moduleList: Locator;
subPages: { name: string; url?: string[] }[];
/**
* @param {Page} page
*/
constructor(page: Page) {
this.page = page;
this.$$moduleList = this.page.locator('.left_box .link_item');
this.subPages = [
{ name: '收银' },
{ name: '预约' },
{ name: '顾客' },
{ name: '流水' },
{ name: '库存' },
{ name: '目标' },
{ name: '营销' },
{ name: '报表' },
{ name: '设置' },
{ name: '其他' },
];
}
/**
*
* @param {string} pageName
* -
* -
* -
* -
* -
* -
* -
* -
* -
* -
*/
gotoModule = async (pageName: string) => {
const $active = this.$$moduleList.locator('.active_arrow');
if (!this.subPages.some(item => item.name === pageName)) {
throw new Error(`Page ${pageName} does not exist`);
}
// 模块定位器
const $module = this.$$moduleList.filter({ hasText: pageName });
// 模块选中按钮
const $moduleActive = $module.locator('.active_arrow');
// 等待选中按钮出现
await $active.waitFor();
await expect(async () => {
if (!(await $moduleActive.isVisible())) {
await $module.click();
}
await $moduleActive.waitFor({ timeout: 5000 });
}).toPass({ timeout: 10_000 });
};
}

View File

@ -0,0 +1,52 @@
import { type Page } from 'playwright';
import { waitSpecifyApiLoad } from '@/utils/utils';
export class InventoryManagementPage {
page: Page;
subPages: { name: string; url?: string[] }[];
/**
*
* @param page
*/
constructor(page: Page) {
this.page = page;
this.subPages = [
{ name: '库存余量表', url: [] },
{ name: '安全库存差额表', url: [] },
{ name: '寄存余量表', url: [] },
{ name: '过期预警表', url: [] },
];
}
/**
*
* @param subPageName
* -
* -
* -
* -
*/
async gotoSubPage(subPageName: string) {
if (!this.subPages.some(item => item.name === subPageName)) {
throw new Error(`暂不支持${subPageName}页面`);
}
const $dropDown = this.page
.locator('.top_tab .tab_item', {
hasText: '库存管理',
})
.locator('.ant-dropdown-link');
await $dropDown.click();
const subPage = this.subPages.find(item => item.name === subPageName);
if (subPage?.url) {
await Promise.all([
this.page.getByRole('menuitem', { name: subPageName }).click(),
waitSpecifyApiLoad(subPage.url),
]);
} else {
await this.page.getByRole('menuitem', { name: subPageName }).click();
}
}
}

View File

@ -0,0 +1,4 @@
import { TransferManagementPage } from './inventoryTransferManagementPage';
import { InventoryManagementPage } from './InventoryManagementPage';
export { TransferManagementPage, InventoryManagementPage };

View File

@ -0,0 +1,43 @@
import { Page } from 'playwright';
import { waitSpecifyApiLoad } from '@/utils/utils';
export class TransferManagementPage {
page: Page;
subPages: { name: string; url?: string[] }[];
/**
* -
* @param {import("@playwright/test").Page} page
*/
constructor(page: Page) {
this.page = page;
this.subPages = [
{ name: '门店要货', url: ['/stock'] },
{ name: '要货单', url: ['/transfer_stock_bill'] },
{ name: '调货管理', url: ['/transfer_stock_bill'] },
];
}
gotoSubPage = async (subPageName: string) => {
if (!this.subPages.some(item => item.name === subPageName)) {
throw new Error(`暂不支持${subPageName}页面`);
}
const $dropDown = this.page
.locator('.top_tab .tab_item', {
hasText: '调货管理',
})
.locator('.ant-dropdown-link');
await $dropDown.click();
const subPage = this.subPages.find(item => item.name === subPageName);
if (subPage?.url) {
await Promise.all([
this.page.getByRole('menuitem', { name: subPageName }).click(),
waitSpecifyApiLoad(subPage.url),
]);
} else {
await this.page.getByRole('menuitem', { name: subPageName }).click();
}
};
}

View File

@ -0,0 +1,4 @@
import { MarketingPage } from './marketingPage';
import { MarketingInviteGuestsPage } from './marketingInviteGuestsPage';
export { MarketingPage, MarketingInviteGuestsPage };

View File

@ -0,0 +1,80 @@
import { type Locator, type Page } from '@playwright/test';
import { waitSpecifyApiLoad } from '@/utils/utils.js';
export class MarketingInviteGuestsPage {
page: Page;
subPages: { name: string; content?: { name: string; url?: string[] }[] }[];
$$tabItem: Locator;
/**
*
* @param page
*/
constructor(page: Page) {
this.page = page;
this.$$tabItem = this.page.locator('.tab_item');
this.subPages = [
{
name: '邀客管理',
},
{
name: '返利方案',
},
{
name: '数据统计',
},
{
name: '设置',
content: [
{ name: '礼品设置', url: [] },
{ name: '礼品分配', url: ['/user_present'] },
{ name: '返利升级方案', url: [] },
{ name: '设置', url: [] },
],
},
];
}
/**
*
* @param subPageName
* -
* -
* -
* -
*/
gotoSubPage = async (subPageName: string) => {
const subPage = this.subPages.find(e => e.name === subPageName);
if (!subPage) {
throw new Error(`${subPageName}不存在`);
}
const $tabItem = this.$$tabItem.filter({ hasText: subPageName });
await $tabItem.click();
};
/**
*
* @param subPageName
* -
* -
* -
* -
*/
gotoSettingSubPage = async (subPageName: string) => {
const $tabItem = this.$$tabItem.filter({ hasText: subPageName });
const $tabItemDropDown = $tabItem.locator('.ant-dropdown-link');
const $dropDownSelected = this.page.getByRole('menuitem', { name: subPageName });
const settingSubPage = this.subPages.find(e => e.name === '设置')?.content?.find(e => e.name === subPageName);
if (!settingSubPage) {
throw new Error(`${subPageName}不存在`);
}
// 打开下拉
await $tabItemDropDown.click();
// 选择下拉选择
await Promise.all([$dropDownSelected.click(), waitSpecifyApiLoad(this.page, settingSubPage.url)]);
// 确认选择成功
await $tabItemDropDown.filter({ hasText: subPageName }).waitFor();
};
}

View File

@ -0,0 +1,83 @@
import { type Page } from '@playwright/test';
export class MarketingPage {
page: Page;
mallSubPages: string[];
strategySubPages: string[];
enterpriseWechatSubPages: string[];
toolSettingSubPages: string[];
constructor(page: Page) {
this.page = page;
this.mallSubPages = ['个性状态', '上架好物', '商城订单', '商城设置', '品牌推广'];
this.strategySubPages = ['抢购', '团购', '邀客系统', '会员权益', '积分', '推广收益', '优惠券', '直播系统'];
this.enterpriseWechatSubPages = ['营销获客', '顾客管理', '多渠道触达', '内容营销库', '多维度数据统计'];
this.toolSettingSubPages = ['微信平台'];
}
/**
*
* @param moduleName
* -
* -
* -
* -
* - 广
*/
gotoMallSubPage = async (moduleName: string) => {
const subReportPage = this.mallSubPages.find(e => e === moduleName);
if (!subReportPage) {
throw new Error(`${moduleName}页面不存在`);
}
await this.page.getByText(subReportPage, { exact: true }).click();
};
/**
*
* @param moduleName
* -
* -
* -
* -
* -
* - 广
* -
* -
*/
gotoStrategySubPage = async (moduleName: string) => {
const subReportPage = this.strategySubPages.find(e => e === moduleName);
if (!subReportPage) {
throw new Error(`${moduleName}页面不存在`);
}
await this.page.getByText(subReportPage, { exact: true }).click();
};
/**
*
* @param moduleName
* -
* -
* -
* -
* -
*/
gotoEnterpriseWechatSubPage = async (moduleName: string) => {
const subReportPage = this.enterpriseWechatSubPages.find(e => e === moduleName);
if (!subReportPage) {
throw new Error(`${moduleName}页面不存在`);
}
await this.page.getByText(subReportPage, { exact: true }).click();
};
/**
*
* @param moduleName
* -
*/
gotoToolSettingSubPage = async (moduleName: string) => {
const subReportPage = this.toolSettingSubPages.find(e => e === moduleName);
if (!subReportPage) {
throw new Error(`${moduleName}页面不存在`);
}
await this.page.getByText(subReportPage, { exact: true }).click();
};
}

View File

@ -0,0 +1,121 @@
import { type Page } from '@playwright/test';
import { reportData } from './reportPage';
export class CardBalanceChangeReportPage {
page: Page;
private readonly _reportIndex: reportData[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this._reportIndex = [
{
name: '期初',
content: [
{ name: '期初卡金', index: -1 },
{ name: '期初赠金', index: -1 },
],
},
{
name: '消费',
content: [
{ name: '消费卡金', index: -1 },
{ name: '消费赠金', index: -1 },
],
},
{
name: '开充卡',
content: [
{ name: '开充卡金', index: -1 },
{ name: '开充赠金', index: -1 },
],
},
{
name: '调整',
content: [
{ name: '修改卡金', index: -1 },
{ name: '修改赠金', index: -1 },
],
},
{
name: '转移',
content: [
{ name: '转移卡金', index: -1 },
{ name: '转移赠金', index: -1 },
],
},
{
name: '结余',
content: [
{ name: '结余卡金', index: -1 },
{ name: '结余赠金', index: -1 },
],
},
{
name: '变动',
content: [
{ name: '卡金变动', index: -1 },
{ name: '赠金变动', index: -1 },
],
},
];
}
get reportIndex() {
return this._reportIndex;
}
/**
*
*/
updateReportDataIndex = async () => {
const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th');
await thLocator.first().waitFor();
const headers = await thLocator.allInnerTexts();
for (const category of this._reportIndex) {
for (const content of category.content!) {
// 第一列是会员卡名称,故+1
content.index =
headers.findIndex(headerText => {
return headerText === content.name;
}) + 1;
}
}
};
/**
*
* @param cardName
* @param cardData
*/
getSpecifyCardReportData = async (cardName: string, cardData: reportData[]) => {
const tdLocator = this.page
.locator('.table_sub-box .m-table__body-wrapper tr', { hasText: cardName })
.first()
.locator('td');
await tdLocator.first().waitFor();
const headers = await tdLocator.allInnerTexts();
for (const category of this._reportIndex) {
const cardCategoryIndex = cardData.findIndex(card => card.name === category.name);
if (cardCategoryIndex === -1) {
// 为空时初始化一个对象
const obj = {
name: category.name,
content: category.content?.map(item => ({ ...item, value: 0, lastValue: 0 })),
};
cardData.push(obj);
}
const cardCategory = cardData.find(card => card.name === category.name);
if (!cardCategory || !cardCategory.content) {
throw new Error(`cardCategory${JSON.stringify(cardCategory)}`);
}
for (const content of cardCategory.content) {
content.lastValue = content.value;
content.value = Number(headers[content.index!] === '--' ? 0 : headers[content.index!]);
}
}
};
}

View File

@ -0,0 +1,103 @@
import { Page } from '@playwright/test';
import { type reportData } from './reportPage';
import { Customer } from '@/utils/customer';
export class CustomerConsumptionAnalysisReportPage {
readonly page: Page;
private _reportData: reportData[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this._reportData = [
{ name: '到店次数', value: 0, lastValue: 0, index: -1 },
{ name: '消费次数', value: 0, lastValue: 0, index: -1 },
{ name: '消费金额', value: 0, lastValue: 0, index: -1 },
{
name: '护理(购买)',
content: [
{ name: '现金', value: 0, lastValue: 0, index: -1 },
{ name: '划卡金', value: 0, lastValue: 0, index: -1 },
],
},
];
}
get reportData() {
return this._reportData;
}
/**
* index
*/
updateReportDataIndex = async () => {
const $firstTr = this.page.locator('.m-table__header-wrapper tr').nth(0).locator('th');
await $firstTr.first().waitFor();
const allFirstTrText = await $firstTr.allInnerTexts();
const $secondTr = this.page.locator('.m-table__header-wrapper tr').nth(1).locator('th');
const allSecondTrText = await $secondTr.allInnerTexts();
for (const category of this._reportData) {
// 找寻一级标题的index
if (typeof category.content === 'undefined') {
category.index = allFirstTrText.findIndex(hedader => hedader === category.name);
}
// 找寻二级标题的index
if (category.content) {
for (const content of category.content) {
content.index = allSecondTrText.findIndex(hedader => hedader === content.name);
}
}
}
};
/**
*
* @param {Customer} customer
*/
updateReportData = async (customer: Customer) => {
await this.page.locator('.main-table-body_tr').filter({ hasText: customer.phone }).waitFor();
const customerDataArray = await this.page
.locator('.main-table-body_tr')
.filter({ hasText: customer.phone })
.locator('td')
.allInnerTexts();
for (const category of this._reportData) {
let value: string;
// 找寻一级标题的index
if (typeof category.content === 'undefined') {
category.lastValue = category.value;
if (category.index) {
value =
customerDataArray[category.index].trim() === '--'
? '0'
: customerDataArray[category.index].trim();
category.value = Number(value);
} else {
throw new Error(`找不到${category.name}的index`);
}
}
if (category.content) {
// 找寻二级标题的index
for (const content of category.content) {
content.lastValue = content.value;
if (content.index) {
value =
customerDataArray[content.index].trim() === '--'
? '0'
: customerDataArray[content.index].trim();
content.value = Number(value);
} else {
throw new Error(`找不到${content.name}的index`);
}
}
}
}
};
}

View File

@ -0,0 +1,19 @@
import { ReportPage } from './reportPage';
import { PerformanceSummaryReportPage } from './performanceSummaryReport';
import { SpendingSummaryReportPage } from './spendingSummaryReport';
import { PerformanceDetailReportPage } from './performanceDetailReport';
import { CardBalanceChangeReportPage } from './cardBalanceChangeReport';
import { ItemSalesConsumptionAccessReportPage } from './itemSalesConsumptionAccessReport';
import { SalesCostSummaryReportPage } from './salesCostSummaryReport';
import { CustomerConsumptionAnalysisReportPage } from './customerConsumptionAnalysisReport';
export {
ReportPage,
PerformanceSummaryReportPage,
SpendingSummaryReportPage,
PerformanceDetailReportPage,
CardBalanceChangeReportPage,
ItemSalesConsumptionAccessReportPage,
SalesCostSummaryReportPage,
CustomerConsumptionAnalysisReportPage,
};

View File

@ -0,0 +1,129 @@
import { type Page } from '@playwright/test';
import { reportData } from './reportPage';
export class ItemSalesConsumptionAccessReportPage {
page: Page;
private readonly _reportDataIndex: reportData[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this._reportDataIndex = [
{
name: '期初',
content: [
{ name: '期初金额', value: 0, lastValue: 0, index: -1 },
{ name: '期初数量', value: 0, lastValue: 0, index: -1 },
{ name: '期初赠送数量', value: 0, lastValue: 0, index: -1 },
],
},
{
name: '销售',
content: [
{ name: '销售金额', value: 0, lastValue: 0, index: -1 },
{ name: '销售数量', value: 0, lastValue: 0, index: -1 },
{ name: '赠送数量', value: 0, lastValue: 0, index: -1 },
{ name: '购买人数', value: 0, lastValue: 0, index: -1 },
{ name: '项目复购率', value: 0, lastValue: 0, index: -1 },
{ name: '项目销售占比', value: 0, lastValue: 0, index: -1 },
],
},
{
name: '消耗',
content: [
{ name: '消耗金额', value: 0, lastValue: 0, index: -1 },
{ name: '消耗数量', value: 0, lastValue: 0, index: -1 },
{ name: '消耗赠送数量', value: 0, lastValue: 0, index: -1 },
{ name: '消耗人数', value: 0, lastValue: 0, index: -1 },
{ name: '项目消耗占比', value: 0, lastValue: 0, index: -1 },
],
},
{
name: '调整',
content: [
{ name: '调整金额', value: 0, lastValue: 0, index: -1 },
{ name: '换出数量', value: 0, lastValue: 0, index: -1 },
{ name: '换入数量', value: 0, lastValue: 0, index: -1 },
{ name: '更改数量', value: 0, lastValue: 0, index: -1 },
{ name: '期初导入数量', value: 0, lastValue: 0, index: -1 },
],
},
{
name: '转移',
content: [
{ name: '转入金额', value: 0, lastValue: 0, index: -1 },
{ name: '转入成本', value: 0, lastValue: 0, index: -1 },
{ name: '转入数量', value: 0, lastValue: 0, index: -1 },
{ name: '转出金额', value: 0, lastValue: 0, index: -1 },
{ name: '转出成本', value: 0, lastValue: 0, index: -1 },
{ name: '转出数量', value: 0, lastValue: 0, index: -1 },
],
},
{
name: '结余',
content: [
{ name: '结余金额', value: 0, lastValue: 0, index: -1 },
{ name: '结余数量', value: 0, lastValue: 0, index: -1 },
{ name: '结余赠送数量', value: 0, lastValue: 0, index: -1 },
{ name: '过期结余金额', value: 0, lastValue: 0, index: -1 },
{ name: '过期结余数量', value: 0, lastValue: 0, index: -1 },
{ name: '过期结余赠送数量', value: 0, lastValue: 0, index: -1 },
],
},
];
}
/**
*
*/
updateReportDataIndex = async () => {
const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th');
await thLocator.first().waitFor();
const headers = await thLocator.allInnerTexts();
for (const category of this._reportDataIndex) {
for (const content of category.content!) {
// 第一列是会员卡名称,故+1
content.index =
headers.findIndex(headerText => {
return headerText === content.name;
}) + 1;
}
}
};
/**
*
* @param itemName
* @param itemData
*/
getSpecifyItemReportData = async (itemName: string, itemData: reportData[]) => {
const tdLocator = this.page
.locator('.table_sub-box .m-table__body-wrapper tr', { hasText: itemName })
.first()
.locator('td');
await tdLocator.first().waitFor();
const headers = await tdLocator.allInnerTexts();
for (const category of this._reportDataIndex) {
const cardCategoryIndex = itemData.findIndex(card => card.name === category.name);
if (cardCategoryIndex === -1) {
// 为空时初始化一个对象
const obj = {
name: category.name,
content: category.content?.map(item => ({ ...item, value: 0, lastValue: 0 })),
};
itemData.push(obj);
}
const cardCategory = itemData.find(card => card.name === category.name);
if (!cardCategory || !cardCategory.content) {
throw new Error(`cardCategory${JSON.stringify(cardCategory)}`);
}
for (const content of cardCategory.content) {
content.lastValue = content.value;
content.value = Number(headers[content.index!] === '--' ? 0 : headers[content.index!]);
}
}
};
}

View File

@ -0,0 +1,85 @@
import { type Locator, type Page } from '@playwright/test';
import { type reportData } from './reportPage';
export class PerformanceDetailReportPage {
page: Page;
private readonly _reportData: reportData[];
private subTables: { name: string; locator: Locator }[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this._reportData = [
{
name: '项目销售',
content: [
{ name: '数量', value: 0, lastValue: 0, index: -1 },
{ name: '金额', value: 0, lastValue: 0, index: -1 },
{ name: '现金业绩', value: 0, lastValue: 0, index: -1 },
],
},
];
this.subTables = [
{ name: '项目销售', locator: this.page.locator('.SERVICE_SALE_table') },
{ name: '项目消耗', locator: this.page.locator('.SERVICE_table') },
{ name: '项目套餐包销售', locator: this.page.locator('.PACKAGE_table') },
{ name: '项目卖品销售', locator: this.page.locator('.GOODS_table') },
{ name: '卡销售', locator: this.page.locator('.CARD_table') },
];
}
get reportData(): reportData[] {
return this._reportData;
}
/**
*
*/
updateReportDataIndex = async () => {
for (const subReportData of this._reportData) {
const subTable = this.subTables.find(subTable => subTable.name === subReportData.name);
if (!subTable || !subTable.locator) {
throw new Error(`${subTable}`);
}
const currentTableLocator = subTable.locator;
await currentTableLocator.waitFor();
const allHeader = await currentTableLocator
.locator('.m-table__header-wrapper tr')
.nth(1)
.locator('th')
.allInnerTexts();
for (const content of subReportData.content!) {
content.index = allHeader.findIndex(headerText => {
return headerText === content.name;
});
}
}
};
/**
*
*/
updateReportDataForTableTotal = async () => {
for (const subReportData of this._reportData) {
const subTable = this.subTables.find(subTable => subTable.name === subReportData.name);
if (!subTable || !subTable.locator) {
throw new Error(`${subTable}`);
}
const currentTableLocator = subTable.locator;
await currentTableLocator.waitFor();
const allFooterText = await currentTableLocator
.locator('.table_sub-box .m-table-footer td')
.allInnerTexts();
for (const content of subReportData.content!) {
// 第一个是合计,故+1
const currentIndex = content.index! + 1;
content.lastValue = content.value;
content.value = Number(
allFooterText[currentIndex].trim() === '--' ? 0 : allFooterText[currentIndex].trim(),
);
}
}
};
}

View File

@ -0,0 +1,143 @@
import { Page } from '@playwright/test';
import { type reportData } from './reportPage';
export class PerformanceSummaryReportPage {
page: Page;
private readonly _reportData: reportData[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this._reportData = [
{
name: '营收明细',
content: [
{ name: '现金类总额', value: 0, lastValue: 0, index: 0 },
{ name: '现金', value: 0, lastValue: 0, index: 0 },
{ name: '划卡', value: 0, lastValue: 0, index: 0 },
{ name: '划赠金', value: 0, lastValue: 0, index: 0 },
],
},
{
name: '开支明细',
content: [{ name: '支出总额', value: 0, lastValue: 0, index: 0 }],
},
{
name: '现金业绩',
content: [
{ name: '项目合计', value: 0, lastValue: 0, index: 0 },
{ name: '卖品业绩', value: 0, lastValue: 0, index: 0 },
{ name: '开充卡业绩', value: 0, lastValue: 0, index: 0 },
{ name: '开卡业绩', value: 0, lastValue: 0, index: 0 },
{ name: '充值业绩', value: 0, lastValue: 0, index: 0 },
{ name: '总现金业绩', value: 0, lastValue: 0, index: 0 },
],
},
{
name: '划卡业绩',
content: [{ name: '项目合计', value: 0, lastValue: 0, index: 0 }],
},
{
name: '消耗业绩',
content: [
{ name: '总消耗业绩', value: 0, lastValue: 0, index: 0 },
{ name: '项目总数', value: 0, lastValue: 0, index: 0 },
],
},
{
name: '客流',
content: [
{ name: '客数', value: 0, lastValue: 0, index: 0 },
{ name: '客次', value: 0, lastValue: 0, index: 0 },
],
},
{
name: '套餐销售',
content: [
{ name: '总数量', value: 0, lastValue: 0, index: 0 },
{ name: '普通数量', value: 0, lastValue: 0, index: 0 },
{ name: '总金额', value: 0, lastValue: 0, index: 0 },
{ name: '总业绩', value: 0, lastValue: 0, index: 0 },
],
},
{
name: '卖品销售',
content: [
{ name: '总数量', value: 0, lastValue: 0, index: 0 },
{ name: '普通数量', value: 0, lastValue: 0, index: 0 },
{ name: '总金额', value: 0, lastValue: 0, index: 0 },
{ name: '总业绩', value: 0, lastValue: 0, index: 0 },
],
},
{
name: '其他指标',
content: [
{ name: '客单价', value: 0, lastValue: 0, index: 0 },
{ name: '总耗业绩', value: 0, lastValue: 0, index: 0 },
],
},
];
}
get reportData() {
return this._reportData;
}
/**
*
*/
updateReportDataIndex = async () => {
const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th');
await thLocator.first().waitFor();
const headers = await thLocator.allInnerTexts();
for (const category of this._reportData) {
for (const content of category.content!) {
let index: number;
let occurrence = 0;
index = headers.findIndex(headerText => {
if (headerText === content.name) {
occurrence++; // 每次找到相同的文本计数加1
if (category.name === '卖品销售' || category.name === '划卡业绩') {
// 找到第二次出现的 content.name
return occurrence === 2;
} else {
// 找到第一次出现的 content.name
return occurrence === 1;
}
}
return false;
});
if (index !== -1) {
content.index = index + 1;
} else {
throw new Error(`没有找到${content.name}`);
}
}
}
};
/**
*
*/
updateReportDataFromTotal = async () => {
// 合计行定位器
const $totalTr = this.page.locator('.m-table-footer .m-table__row');
await $totalTr.first().waitFor();
// 拿取当前的数据
const $$totalTd = $totalTr.locator('td');
const totalAllText = await $$totalTd.allInnerTexts();
if (totalAllText.length === 0) {
return new Error(`${$totalTr.toString()}没有找到`);
}
for (const category of this._reportData) {
for (const content of category.content!) {
content.lastValue = content.value;
content.value = Number(
totalAllText[content.index].trim() === '--' ? 0 : totalAllText[content.index].trim(),
);
}
}
};
}

View File

@ -0,0 +1,181 @@
import { expect, type Locator, type Page } from '@playwright/test';
import { waitSpecifyApiLoad } from '@/utils/utils.js';
type subReportPage = {
name: string;
url: string[];
};
export type reportData = {
name: string;
value?: number;
lastValue?: number;
index?: number;
content?: reportDataInfo[];
};
export type reportDataInfo = {
name: string;
value: number;
lastValue: number;
index: number;
};
export class ReportPage {
page: Page;
private subPages: subReportPage[] = [
{ name: '业绩汇总表', url: ['/21'] },
{ name: '业绩明细表', url: ['/5'] },
{ name: '项目销耗存表', url: ['/22'] },
{ name: '销售消耗汇总表', url: ['/23'] },
{ name: '顾客消费分析表', url: ['/24'] },
{ name: '储值卡卡金变动表', url: ['/6'] },
{ name: '开支汇总表', url: ['/7'] },
{ name: '顾客分诊明细表', url: ['/101'] },
];
private $closeCustomReportPage: Locator;
private $closeRecommendReportPage: Locator;
/**
*
*/
constructor(page: Page) {
this.page = page;
this.$closeCustomReportPage = this.page
.locator('div')
.filter({ hasText: /^自定义报表$/ })
.locator('svg');
this.$closeRecommendReportPage = this.page
.locator('div')
.filter({ hasText: /^推荐报表$/ })
.locator('svg');
}
/**
* @param subPageName
* -
* -
* -
* -
* -
* -
* -
* -
*/
gotoSubPage = async (subPageName: string) => {
const subPage = this.subPages.find(e => e.name === subPageName);
if (!subPage) {
throw new Error(`子页面 ${subPageName} 不存在`);
}
await Promise.all([
this.page.locator('.m-report_recommend .item').filter({ hasText: subPage.name }).click(),
waitSpecifyApiLoad(this.page, subPage.url),
]);
await expect(this.page.locator('span').filter({ hasText: '推荐报表' })).toBeVisible();
};
/**
*
*/
closeCustomReportPage = async () => {
await this.$closeCustomReportPage.click();
};
/**
*
*/
closeRecommendReportPage = async () => {
await this.$closeRecommendReportPage.click();
};
/**
*
* @param mainClassName
* @param secondaryClassName
* @param reportData
* @param expected
*/
toBeReportDataAsExpected = (
mainClassName: string,
secondaryClassName: string,
reportData: reportData[],
expected: number,
) => {
const mainClass = reportData.find(e => e.name === mainClassName);
if (!mainClass) {
throw new Error('mainClass值不对');
}
if (
secondaryClassName === '' ||
(mainClass?.content?.length === 0 && typeof mainClass.content === 'undefined')
) {
if (mainClass.value === undefined || mainClass.lastValue === undefined) {
throw new Error(`mainClass${mainClassName} 的值或 lastValue 未定义`);
}
expect.soft(mainClass.value - mainClass.lastValue).toBe(expected);
} else {
if (!mainClass.content) {
throw new Error(`mainClass${mainClass.content}`);
}
const secondaryClass = mainClass.content.find(e => e.name === secondaryClassName);
if (!secondaryClass) {
throw new Error(`secondaryClass${secondaryClassName}`);
}
expect.soft(secondaryClass.value! - secondaryClass.lastValue!).toBe(expected);
}
};
/**
*
* @param subReportPageName
* -
* -
* -
* -
* -
* -
* -
* -
*/
openCustomReportPage = async (subReportPageName: string) => {
const subReportPage = this.subPages.find(e => e.name === subReportPageName);
if (!subReportPage) {
throw new Error(`子页面 ${subReportPageName} 不存在`);
}
await this.page
.locator('.m-report_recommend .item')
.filter({ hasText: `${subReportPageName}` })
.locator('.opreate')
.click();
};
/**
*
* @param reportName
*/
deleteCustomReportPage = async (reportName: string) => {
await this.page
.locator('.m-report_custorm .item')
.filter({ has: this.page.getByText(reportName) })
.locator('.arrow')
.click(); // 点击箭头
await this.page.getByRole('menuitem', { name: '删除' }).click(); // 点击删除
await this.page.getByRole('button', { name: /确\s认/ }).click(); // 点击确认
};
deleteAllCustomReportPage = async () => {
const $$item = this.page.locator('.m-report_custorm .item');
const itemsCount = await this.page.locator('.m-report_custorm .item').count(); // 获取所有符合条件的元素
for (let i = 0; i < itemsCount; i++) {
await $$item.first().locator('.arrow').click(); // 点击箭头
await this.page.getByRole('menuitem', { name: '删除' }).click(); // 点击删除
await this.page.getByRole('button', { name: /确\s认/ }).click(); // 点击确认
}
};
}

View File

@ -0,0 +1,87 @@
import { Page } from '@playwright/test';
import { reportData } from './reportPage';
export class SalesCostSummaryReportPage {
page: Page;
_reportData: reportData[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this._reportData = [
{
name: '护理',
content: [
{ name: '现金业绩', index: -1 },
{ name: '划卡业绩', index: -1 },
{ name: '消耗业绩', index: -1 },
],
},
{
name: '面部',
content: [
{ name: '现金业绩', index: -1 },
{ name: '划卡业绩', index: -1 },
{ name: '消耗业绩', index: -1 },
],
},
{
name: '身体',
content: [
{ name: '现金业绩', index: -1 },
{ name: '划卡业绩', index: -1 },
{ name: '消耗业绩', index: -1 },
],
},
{
name: '组合项目',
content: [
{ name: '现金业绩', index: -1 },
{ name: '划卡业绩', index: -1 },
{ name: '消耗业绩', index: -1 },
],
},
];
}
get reportData() {
return this._reportData;
}
updateReportDataIndex = async () => {
const thLocator = this.page.locator('.m-table__header-wrapper tr').first().locator('th[colspan="3"]');
await thLocator.first().waitFor();
const headers = await thLocator.allInnerTexts();
for (let i = 0; i < headers.length; i++) {
const reportDataTmp = this._reportData.find(e => e.name === headers[i]);
if (!reportDataTmp || !reportDataTmp.content) {
throw new Error(`${reportDataTmp}--${reportDataTmp?.content}`);
}
for (let j = 0; j < 3; j++) {
reportDataTmp.content[j].index = i * 3 + j;
}
}
};
updateReportData = async (store: number): Promise<void> => {
// 没有总部故-1
const trLocator = this.page.locator('.main-table-body_tr').nth(store - 1);
await trLocator.first().waitFor();
const tdLocator = trLocator.locator('td');
const headers = await tdLocator.allInnerTexts();
if (headers.length === 0) {
throw new Error('没有获取到数据');
}
for (const category of this._reportData) {
for (const content of category.content!) {
// 保存上一次的数据
content.lastValue = content.value;
// 拿取当前的数据currentIndex第一列为门店故+1
const currentIndex = content.index! + 1;
content.value = Number(headers[currentIndex].trim() === '--' ? 0 : headers[currentIndex].trim());
}
}
};
}

View File

@ -0,0 +1,89 @@
import { Page } from '@playwright/test';
import { reportData } from './reportPage';
export class SpendingSummaryReportPage {
page: Page;
_reportData: reportData[];
/**
*
*/
constructor(page: Page) {
this.page = page;
this._reportData = [
{
name: '支出汇总',
content: [
{ name: '累计支出', value: 0, lastValue: 0, index: -1 },
{ name: '营业收入支出', value: 0, lastValue: 0, index: -1 },
{ name: '备用金支出', value: 0, lastValue: 0, index: -1 },
],
},
{
name: '日常开支',
content: [{ name: '顾客餐', value: 0, lastValue: 0, index: -1 }],
},
];
}
get reportData(): reportData[] {
return this._reportData;
}
/**
* index
*/
updateReportDataIndex = async (): Promise<void> => {
const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th');
await thLocator.first().waitFor();
const headers = await thLocator.allInnerTexts();
for (const category of this._reportData) {
for (const content of category.content!) {
// 拿取th.name列的列数
let index = -1;
let occurrence = 0;
index = headers.findIndex(headerText => {
if (headerText === content.name) {
occurrence++; // 每次找到相同的文本计数加1
if (category.name === '卖品销售') {
// 找到第二次出现的 content.name
return occurrence === 2;
} else {
// 找到第一次出现的 content.name
return occurrence === 1;
}
}
return false;
});
if (index !== -1) {
content.index = index + 1;
console.log(`${content.name} 列是第 ${index + 1}`);
} else {
throw new Error(`没有找到${content.name}`);
}
}
}
};
/**
*
* @param {1|2} store
*/
updateReportData = async (store: number): Promise<void> => {
const trLocator = this.page.locator('.main-table-body_tr').nth(store);
await trLocator.first().waitFor();
for (const category of this._reportData) {
for (const content of category.content!) {
// 保存上一次的数据
content.lastValue = content.value;
// 拿取当前的数据
const tdLocator = trLocator.locator('td');
const headers = await tdLocator.allInnerTexts();
if (headers.length === 0) {
throw new Error('没有获取到数据');
}
content.value = Number(headers[content.index!].trim() === '--' ? 0 : headers[content.index!].trim());
}
}
};
}

208
tests/pages/tablePage.ts Normal file
View File

@ -0,0 +1,208 @@
import { type Locator, type Page } from 'playwright';
export class TablePage {
page: Page;
private readonly _$headerTable: Locator;
private readonly _$bodyTable: Locator;
private readonly _$fixedLeftTable: Locator;
private readonly _$fixedRightTable: Locator;
private readonly _$$bodyTrTable: Locator;
/**
* Initializes a new instance of the TablePage class.
*
* @param page - The Playwright Page object used to interact with the table elements on the page.
*/
constructor(page: Page) {
this.page = page;
this._$headerTable = page.locator('.m-table__header-wrapper');
this._$bodyTable = page.locator('.m-table__body-wrapper');
this._$fixedLeftTable = page.locator('.m-table__fixed-left');
this._$fixedRightTable = page.locator('.m-table__fixed-right');
this._$$bodyTrTable = this._$bodyTable.locator('.main-table-body_tr');
}
get fixedRightTable() {
return this._$fixedRightTable;
}
get fixedLeftTable() {
return this._$fixedLeftTable;
}
get bodyTable() {
return this._$bodyTable;
}
get bodyTrTable() {
return this._$$bodyTrTable;
}
get headerTable() {
return this._$headerTable;
}
/**
*
* @param text
* @param options
* @returns
*/
getFirstHeaderTableIndex = async (text: string, options?: { repeat: number }) => {
const $tr = this._$headerTable.locator('tr').first();
const $$th = $tr.locator('th');
await $$th.first().waitFor();
const firstHeaderText = await $$th.allInnerTexts();
if (options?.repeat) {
return this.findNthIndex(firstHeaderText, text, options.repeat);
}
return firstHeaderText.findIndex(item => item === text);
};
/**
*
* @param text
* @param options
* @returns
*/
getSecondHeaderTableIndex = async (text: string, options?: { repeat: number }) => {
const $tr = this._$headerTable.locator('tr').nth(1);
const $$th = $tr.locator('th');
await $$th.first().waitFor();
const secondHeaderText = await $$th.allInnerTexts();
if (options?.repeat) {
return this.findNthIndex(secondHeaderText, text, options.repeat);
}
return secondHeaderText.findIndex(item => item === text);
};
/**
*
* @param text
* @param options
* @returns
*/
getLeftTableHeaderIndex = async (text: string, options?: { repeat: number }) => {
const $tr = this._$fixedLeftTable.locator('.m-table-fixed-header').locator('thead tr').first();
const $$th = $tr.locator('th');
await $$th.first().waitFor();
const leftTableHeaderText = await $$th.allInnerTexts();
if (options?.repeat) {
return this.findNthIndex(leftTableHeaderText, text, options.repeat);
}
return leftTableHeaderText.findIndex(item => item === text);
};
/**
*
*
* <tbody><tr><tr>
* <tr>
* -1
* @param text
* @param index getLeftTableHeaderIndex
* @returns
*/
getLeftTableBodyIndex = async (text: string, index: number, options?: { repeat: number }) => {
const $$tr = this._$fixedLeftTable.locator('.m-table-fixed-body').locator('tbody tr');
const $$td = $$tr.locator(`td:nth-child(${index + 1})`);
await $$td.first().waitFor();
const leftTableBodyTdText = await $$td.allInnerTexts();
if (options?.repeat) {
return this.findNthIndex(leftTableBodyTdText, text, options.repeat);
}
return leftTableBodyTdText.findIndex(item => item === text);
};
/**
*
* @param text
* @param options
* @returns
*/
getRightTableHeaderIndex = async (text: string, options?: { repeat: number }) => {
const $tr = this._$fixedRightTable.locator('.m-table-fixed-header').locator('thead tr').first();
const $$th = $tr.locator('th');
await $$th.first().waitFor();
const rightTableHeaderText = await $$th.allInnerTexts();
if (options?.repeat) {
return this.findNthIndex(rightTableHeaderText, text, options.repeat);
}
return rightTableHeaderText.findIndex(item => item === text);
};
/**
*
* @param text
* @param index
* @param options
* @returns
*/
getRightTableBodyIndex = async (text: string, index: number, options?: { repeat: number }) => {
const $$tr = this._$fixedRightTable.locator('.m-table-fixed-body').locator('tbody tr');
const $$td = $$tr.locator(`tr:nth-child(${index})`);
await $$td.first().waitFor();
const rightTableBodyTdText = await $$td.allInnerTexts();
if (options?.repeat) {
return this.findNthIndex(rightTableBodyTdText, text, options.repeat);
}
return rightTableBodyTdText.findIndex(item => item === text);
};
/**
*
* @param keywords
* @returns
*/
getBodyTableIndex = async (keywords: string[]) => {
const $$tr = this._$bodyTable.locator('tbody tr');
await $$tr.first().waitFor();
const textArrayCount = await $$tr.count();
for (let i = 0; i < textArrayCount; i++) {
const temp = await $$tr.nth(i).locator('td').allInnerTexts();
const allInnerTexts = keywords.every(text => temp.includes(text));
if (allInnerTexts) {
return i;
}
}
return -1;
};
/**
* n个指定值的索引
* @param arr
* @param value
* @param n
* @returns -1
*/
private findNthIndex(arr: string[], value: string, n: number) {
let count = 0;
let result = -1;
arr.some((item, index) => {
if (item === value) count++;
if (count === n) {
result = index;
return true;
}
return false;
});
return result;
}
}

View File

@ -0,0 +1,3 @@
import { WasteBookBusinessRecordPage } from "./wasteBookBusinessRecordPage";
export { WasteBookBusinessRecordPage };

View File

@ -0,0 +1,71 @@
import { Locator, Page } from 'playwright';
import { waitSpecifyApiLoad } from '@/utils/utils';
export class WasteBookBusinessRecordPage {
page: Page;
$homePage: Locator;
subPages: { name: string; url?: string[] }[];
/**
* -
* @param {import("@playwright/test").Page} page
*/
constructor(page: Page) {
this.page = page;
this.$homePage = this.page.locator('.top_tab .tab_item', {
hasText: '营业记录',
});
this.subPages = [
{ name: '营业记录', url: ['/settled_flow'] },
{ name: '业绩流水', url: ['/perf_flow'] },
{ name: '对账流水', url: ['/payment_flow'] },
];
}
/**
*
* @param subPageName
* -
* -
* -
*/
gotoSubPage = async (subPageName: string) => {
const subPage = this.subPages.find(item => item.name === subPageName);
if (!subPage) {
throw new Error(`未找到${subPageName}页面`);
}
const $dropDown = this.$homePage.locator('.ant-dropdown-link');
await $dropDown.click();
const $subPage = this.page.getByRole('menuitem', { name: subPageName });
await Promise.all([$subPage.click(), waitSpecifyApiLoad(this.page, subPage.url)]);
};
/**
* billSet进行撤单操作
* billSet夹具使用baseFixture
*/
revokeOrder = async (billSet: Set<string>) => {
// 撤掉在预约中开的单
if (billSet.size === 0) {
return;
}
await this.page.getByText('已结算', { exact: true }).waitFor();
for (const bill of billSet) {
await this.page.getByRole('button').click();
await this.page.getByPlaceholder('输入流水单号搜索').fill(bill);
await this.page.locator('.search_btn > svg').click();
const $fixedRightTable = this.page.locator('.m-table__fixed-right');
await $fixedRightTable.locator('td').nth(1).getByText('撤单').click();
await this.page.getByPlaceholder('请输入1-100个字符备注内容').click();
await this.page.getByPlaceholder('请输入1-100个字符备注内容').fill('撤单');
await this.page.getByRole('button', { name: /确\s认/ }).click();
try {
await this.page.locator('.ant-message', { hasText: '撤单成功' }).waitFor({ timeout: 3000 });
} catch (error) {
console.log(`撤单失败,流水单号:${bill}`);
await this.page.getByRole('button', { name: '我知道了' }).click();
}
}
};
}

View File

@ -0,0 +1,70 @@
import { test as setup } from '@playwright/test';
import { readIndexedDB } from '@/utils/indexedDBUtils';
import fs from 'fs';
const firstAuthFile = '.auth/admin_first.json';
const secondAuthFile = '.auth/admin_second.json';
const regex = /^https?:\/\/(www\.)?hlk\.meiguanjia\.net\/?(#\s)?$/;
/** @param personalizedPages 营销-个性化页面*/
const personalizedPages = '/#/marketing/brand/personalized';
class testAccount {
constructor(account, password, path) {
this.account = account;
this.password = password;
this.path = path;
}
account;
password;
path;
}
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;
const $phonePassIcon = page
.locator('div', { has: page.getByRole('textbox', { name: '请输入您的手机号码' }) })
.locator('.pass_svg');
await page.goto(baseURL);
await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(account);
await page.getByRole('textbox', { name: '请输入登录密码' }).fill(password);
await page.getByLabel('请同意慧来客隐私政策和用户协议').check();
await $phonePassIcon.waitFor();
await page.getByRole('button', { name: /登\s录/ }).click();
await page.getByRole('button', { name: /开\s单/ }).waitFor();
if (regex.test(baseURL)) {
await page.goto(personalizedPages);
await page.getByRole('button', { name: '新增模块' }).waitFor();
const $sideIframe = page.locator('.side iframe').contentFrame();
const $logo = $sideIframe.locator('.bar_item', { hasText: '我的' }).locator('.logo');
await $logo.waitFor();
await page.reload();
await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(account);
await page.getByRole('textbox', { name: '请输入登录密码' }).fill(password);
await page.getByLabel('请同意慧来客隐私政策和用户协议').check();
await $phonePassIcon.waitFor();
await page.getByRole('button', { name: /登\s录/ }).click();
await page.getByRole('button', { name: /开\s单/ }).waitFor();
}
const result = await page.evaluate(async readIndexedDBFnString => {
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 });
});
}

View File

@ -0,0 +1,31 @@
import { test as setup, expect } from "@playwright/test";
const authFileArray = [".auth/user_1.json", ".auth/user_2.json"];
const employee = [
{
phone: process.env.staff1_account,
password: process.env.staff1_password,
},
{
phone: process.env.staff2_account,
password: process.env.staff2_password,
},
];
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录/ });
await page.goto(baseUrl);
await $phone.fill(employee[i].phone);
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 $login.click();
await expect(page.getByRole("button", { name: /开\s单/ })).toBeEnabled();
await page.context().storageState({ path: authFileArray[i] });
});
}
});

View File

@ -0,0 +1,638 @@
// @ts-check
import { faker } from '@faker-js/faker/locale/zh_CN';
import { test, expect } from '@/fixtures/boss_common.js';
import path from 'path';
import fs from 'fs';
import { staffData } from '@/fixtures/staff.js';
import { HomeNavigation } from '@/pages/homeNavigationPage.js';
import { AppointmentPage } from '@/pages/appointmentPage.js';
import { AppointmentOperation } from '@/pages/appointmentPage.js';
test('使用预约单元格', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => {
const setAppointmentTime = appointmentPage.getAppointmentTimesAvailable();
const customer = createCustomer;
const employee = staffData.firstStore.firstSector.employee_1;
// 当前可预约时间定位器
const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime });
// 顾客预约定位器
const $customerAppointment = page
.locator('.a_userInfo')
.filter({ hasText: customer.phone })
.filter({ hasText: customer.username });
await test.step('进行未指定预约', async () => {
await homeNavigation.gotoModule('预约');
// 将时间盒子滚动到当前窗口可见
await page.locator('.timeLine_Start').scrollIntoViewIfNeeded();
await $time.scrollIntoViewIfNeeded();
// 打开未指定预约单元格
await appointmentPage.openAppointmentCell('未指定');
// 新建预约
await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT);
// 选择顾客A
await customerPage.searchCustomer(customer.phone);
await customerPage.selectSearchCustomer(customer.phone);
// 确认新建预约
await page.getByRole('button', { name: '确认新建' }).click();
// 判断新建预约成功
await expect($customerAppointment).toBeVisible();
await appointmentPage.openAppointmentDetail(customer.phone);
await expect(page.getByText('未到店', { exact: true })).toBeVisible();
await appointmentPage.closeAppointmentDetail();
});
await test.step('取消未指定预约', async () => {
// 打开未指定预约单元格
await appointmentPage.openAppointmentDetail(customer.phone);
await page.getByRole('button', { name: '取消预约' }).click();
// 确认取消预约
await Promise.all([
page.getByRole('button', { name: /确\s认/ }).click(),
page.waitForResponse(response => response.url().includes('/reservation_num') && response.status() === 200),
]);
// 判断取消预约成功
await expect($customerAppointment).not.toBeVisible();
});
await test.step('指定员工进行预约', async () => {
await $time.scrollIntoViewIfNeeded();
// 打开预约选择窗口
await expect(async () => {
await appointmentPage.openAppointmentCell(employee.name);
await expect(page.locator('.popup_content', { hasText: '选择操作' })).toBeVisible({ timeout: 2000 });
}).toPass();
// 新建预约
await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT);
await customerPage.searchCustomer(customer.phone);
await customerPage.selectSearchCustomer(customer.phone);
// 在预约窗口确认顾客信息
await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone);
await expect(
page.locator('.content .right .right_ul_li', {
hasText: employee.name,
}),
).toBeVisible();
// 判断新建预约成功
await page.getByRole('button', { name: '确认新建' }).click();
await expect($customerAppointment).toBeVisible();
await appointmentPage.openAppointmentDetail(customer.phone);
await expect(page.getByText('未到店', { exact: true })).toBeVisible();
await appointmentPage.closeAppointmentDetail();
});
await test.step('取消预约', async () => {
await page.locator('.a_userInfo', { hasText: customer.username }).first().locator('.user_name_info').click();
await page.getByRole('button', { name: '取消预约' }).click();
await page.getByRole('button', { name: /确\s认/ }).click();
await expect($customerAppointment).not.toBeVisible();
});
});
test('占用预约单元格', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => {
// 获取当前可预约时间
let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable();
// 创建顾客
const customer = createCustomer;
// 员工王芳
const employee = staffData.firstStore.firstSector.employee_3;
// 占用单元格备注
const remark = '占用预约单元格' + faker.string.alpha(2);
// 当前可预约时间定位器
const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime });
// 员工定位器
const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name });
// 顾客预约定位器
const $customerAppointment = page
.locator('.a_userInfo')
.filter({ hasText: customer.phone })
.filter({ hasText: customer.username });
await test.step('占用单元格', async () => {
// 进入预约模块
await homeNavigation.gotoModule('预约');
// 窗口滚动到员工、预约时间可见
await $time.scrollIntoViewIfNeeded();
await $employee.scrollIntoViewIfNeeded();
// 长按目标位置
await appointmentPage.openAppointmentCell(employee.name);
// 进行占用单元格
await Promise.all([
appointmentPage.operationAppointment(AppointmentOperation.ADD_OCCUPY),
page.waitForResponse(response => response.url().includes('/reservation') && response.status() === 200),
]);
// 占用成功
await expect(page.locator('.ant-message', { hasText: '占用成功' })).toBeVisible();
});
await test.step('备注占用单元格', async () => {
// 特殊等待,页面内容已变化,但是去操作时,变为上一个状态,只能等待再去操作
await page.waitForTimeout(2000);
// 打开占用操作窗口
await page.locator('.a_userInfo .occupy', { hasText: '占用' }).last().click();
// 进入添加备注
await appointmentPage.operationAppointment(AppointmentOperation.ADD_REMARK);
// 进行添加备注,确认添加备注成功
await page.getByPlaceholder('请输入备注').fill(remark);
await Promise.all([
page
.locator('.popup_content')
.getByRole('button', { name: /确\s认/ })
.click(),
page.waitForResponse(response => response.url().includes('/reservation_num') && response.status() === 200),
]);
// 判断备注后的单元格存在
await expect(page.locator('.a_userInfo .occupy', { hasText: remark })).toBeVisible();
});
await test.step('取消占用单元格', async () => {
// 打开占用操作窗口
await page.locator('.a_userInfo .occupy', { hasText: remark }).click();
// 取消占用
await appointmentPage.operationAppointment(AppointmentOperation.CANCEL_OCCUPY);
await expect(async () => {
const confirm = page.locator('.popup_content').getByRole('button', { name: /确\s认/ });
await confirm.click();
await expect(confirm).not.toBeVisible();
}).toPass();
// 判断取消占用成功
await expect(page.locator('.ant-message', { hasText: '取消占用成功' })).toBeVisible();
await expect(page.locator('.a_userInfo .occupy', { hasText: remark })).not.toBeVisible();
});
await test.step('取消占用后,能够进行预约', async () => {
// 重新选择员工王芳,进行预约单元格
await $employee.scrollIntoViewIfNeeded();
await $time.scrollIntoViewIfNeeded();
// 打开预约单元格
await appointmentPage.openAppointmentCell(employee.name);
// 选择顾客去创建预约
await expect(async () => {
await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT);
await page.getByText('选择会员').waitFor({ timeout: 2000 });
}).toPass();
await customerPage.searchCustomer(customer.phone);
await customerPage.selectSearchCustomer(customer.phone);
// 确认进入新建预约页面
await expect(
page.locator('.content .right .right_ul_li', {
hasText: employee.name,
}),
).toBeVisible();
await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约');
await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone);
// 新建后,确认新建成功
await page.getByRole('button', { name: '确认新建' }).click();
await expect($customerAppointment).toBeVisible();
});
});
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 $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime });
// 员工定位器
const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name });
// 创建顾客
const customer = createCustomer;
// 获取设置的预约状态
let appointmentStatusSetting;
await test.step('获取预约状态', async () => {
await homeNavigation.gotoModule('预约');
appointmentStatusSetting = await appointmentPage.getAppointmentStatusSetting();
});
await test.step('进行创建预约', async () => {
await $time.scrollIntoViewIfNeeded();
await $employee.scrollIntoViewIfNeeded();
// 打开预约单元格,进行预约
await appointmentPage.openAppointmentCell(employee.name, undefined, 10);
await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT);
await customerPage.searchCustomer(customer.phone);
await customerPage.selectSearchCustomer(customer.phone);
await expect.soft(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约');
await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone);
await page.getByRole('button', { name: '确认新建' }).click();
await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible();
});
await test.step('判断预约,状态为未到店', async () => {
const { customerColor, customerStatus } = await appointmentPage.getCustomerAppointmentStatus(customer);
// 进入未到店状态
expect.soft(customerColor).toEqual(appointmentStatusSetting.NORMAL.color);
expect(customerStatus).toEqual(appointmentStatusSetting.NORMAL.name);
});
await test.step('进行挂单,状态为已开单', async () => {
await page
.locator('.a_userInfo', { hasText: customer.username })
.first()
.locator('.user_name_info')
.click();
// 预约进行挂单
await page.getByRole('button', { name: '到店开单' }).click();
await page
.locator('.project_list')
.filter({ hasText: project.name })
.filter({ hasText: project.no })
.click();
await page.locator('.hold_bill', { hasText: '挂单' }).click();
await expect(page.locator('.ant-message', { hasText: '挂单成功' })).toBeVisible();
await homeNavigation.gotoModule('预约');
// 获取当前预约的状态
const { customerColor, customerStatus } = await appointmentPage.getCustomerAppointmentStatus(customer);
// 进入未到店状态
expect.soft(customerColor).toEqual(appointmentStatusSetting.BILL.color);
expect(customerStatus).toEqual(appointmentStatusSetting.BILL.name);
});
await test.step('进行结算,状态为已结算', async () => {
await page
.locator('.a_userInfo', { hasText: customer.username })
.first()
.locator('.user_name_info')
.click();
await page.getByRole('button', { name: /取\s单/ }).click();
await page.locator('.pay_btn', { hasText: /结\s算/ }).click();
// 取消推送消费提醒和结算签字
await page.getByLabel('推送消费提醒').uncheck();
await page.getByLabel('结算签字').uncheck();
await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click();
// 结算
const [response] = await Promise.all([
page.waitForResponse(async res => {
return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS';
}),
page.getByRole('button', { name: /结\s算/ }).click(),
]);
const responseBody = await response.json();
const billNo = responseBody?.content?.billNo;
expect(billNo).not.toBeNull();
billSet.add(billNo);
// 处理结算后的弹窗
await page.addLocatorHandler(page.getByRole('button', { name: '我知道了' }), async () => {
await page.getByRole('button', { name: '我知道了' }).click();
await expect(page.getByRole('button', { name: '我知道了' })).not.toBeVisible();
await page.reload();
await homeNavigation.gotoModule('预约');
});
// 进入预约模块
await homeNavigation.gotoModule('预约');
// 获取当前创建预约的状态
const { customerColor, customerStatus } = await appointmentPage.getCustomerAppointmentStatus(customer);
// 进入未到店状态
expect.soft(customerColor).toEqual(appointmentStatusSetting.SETTLED.color);
expect(customerStatus).toEqual(appointmentStatusSetting.SETTLED.name);
});
});
test.skip('预约-过期', async ({ page, createCustomer, appointmentPage, customerPage }) => {
// 获取预约时间
let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable();
// 创建顾客
const customer = createCustomer;
// 员工赵军
const employee = staffData.firstStore.firstSector.employee_5;
// 预约时间定位器
const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime });
// 员工定位器
const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name });
await test.step('选择顾客进行预约', async () => {
await $employee.scrollIntoViewIfNeeded();
await $time.scrollIntoViewIfNeeded();
await appointmentPage.openAppointmentCell(employee.name);
await expect(async () => {
await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT);
await expect(page.getByText('选择会员')).toBeVisible();
}).toPass();
await customerPage.searchCustomer(customer.phone);
await customerPage.selectSearchCustomer(customer.phone);
await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约');
await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone);
await page.getByRole('button', { name: '确认新建' }).click();
await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible();
});
});
});
test.describe.skip('预约设置', () => {
test('看板设置', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => {
let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable();
const customer = createCustomer;
const employee = staffData.firstStore.firstSector.employee_6;
let switchStatus; //
let nowNormalColor; // 当前模块颜色
let nowAppointmentColor; //当前预约颜色
const customerRemarkLocator = page
.locator('.appointment_content', { hasText: customer.username })
.locator('.remark_info', { hasText: '测试看板设置' });
const normal = page.locator('.herder_right ul > li', { hasText: '未到店' });
const merchantRemarkLocator = page.locator('li').filter({ hasText: '商家备注' });
const $time = page.locator('.times_table td');
const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name });
await test.step('创建预约', async () => {
await homeNavigation.gotoModule('预约');
await $time.filter({ hasText: setAppointmentTime }).scrollIntoViewIfNeeded();
await $employee.scrollIntoViewIfNeeded();
await appointmentPage.openAppointmentCell(employee.name);
await page.locator('.popup_content', { hasText: '选择操作' }).waitFor();
await expect(async () => {
await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT);
await page.getByText('选择会员').waitFor({ timeout: 2000 });
}).toPass();
await customerPage.searchCustomer(customer.phone);
await customerPage.selectSearchCustomer(customer.phone);
await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约');
await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone);
await page.getByRole('button', { name: '确认新建' }).click();
await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible();
});
await test.step('给顾客预约添加备注', async () => {
await page
.locator('.a_userInfo', { hasText: customer.username })
.first()
.locator('.user_name_info')
.click();
await page.locator('.my_remark .write').click();
await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试看板设置');
await page.getByRole('button', { name: /确\s认/ }).click();
await page.locator('.info_center .close').click();
await expect(customerRemarkLocator).toBeVisible();
});
await test.step('修改商家备注为关闭、修改未到店的颜色', async () => {
await page.getByRole('button', { name: /设\s置/ }).click();
await page.getByRole('menuitem', { name: '看板设置' }).click();
let switchStatus = page.locator('li').filter({ hasText: '商家备注' }).getByRole('switch');
await expect(switchStatus).toHaveText('开');
await page.locator('li').filter({ hasText: '商家备注' }).getByRole('switch').click();
await expect(switchStatus).toHaveText('关');
// 修改未到店的颜色
await page
.locator('li')
.filter({ hasText: /^未到店$/ })
.locator('div')
.nth(3)
.click();
// rgb(0, 0, 0)
// D42525
await page.getByLabel('r', { exact: true }).fill('0');
await page.getByLabel('g', { exact: true }).fill('0');
await page.getByLabel('b', { exact: true }).fill('0');
await page.getByText('看板信息').click();
await page.locator('.close > svg > use').first().click();
// 获取预约状态Color
nowNormalColor = await normal.locator('.status').evaluate(e => {
return window.getComputedStyle(e).backgroundColor;
});
// 获取当前创建预约的状态
nowAppointmentColor = await page
.locator('.a_userInfo', { hasText: customer.username })
.first()
.locator('.appointment')
.evaluate(e => {
return window.getComputedStyle(e).backgroundColor;
});
// 判断商家备注被关闭
await expect(customerRemarkLocator).not.toBeVisible();
// 判断未到店颜色被修改
expect.soft(nowNormalColor).toEqual('rgb(0, 0, 0)');
// 判断预约颜色被修改
expect(nowAppointmentColor).toEqual('rgb(0, 0, 0)');
});
await test.step('恢复默认值', async () => {
// 恢复默认
await page.getByRole('button', { name: /设\s置/ }).click();
await page.getByRole('menuitem', { name: '看板设置' }).click();
switchStatus = merchantRemarkLocator.getByRole('switch');
await expect(switchStatus).toHaveText('关');
await merchantRemarkLocator.getByRole('switch').click();
switchStatus = merchantRemarkLocator.getByRole('switch');
await expect(switchStatus).toHaveText('开');
await page
.locator('li')
.filter({ hasText: /^未到店$/ })
.locator('span')
.nth(1)
.click();
// await page.getByLabel('hex', { exact: true }).fill("D42525");
await page.getByLabel('r', { exact: true }).fill('212');
await page.getByLabel('g', { exact: true }).fill('37');
await page.getByLabel('b', { exact: true }).fill('37');
await page.getByText('状态颜色').click();
await page.locator('.m_sliding_bg').click();
// 获取预约状态Color
nowNormalColor = await normal.locator('.status').evaluate(e => {
return window.getComputedStyle(e).backgroundColor;
});
expect(nowNormalColor).toEqual('rgb(212, 37, 37)');
});
});
test('导出图片', async ({ page, homeNavigation }) => {
await homeNavigation.gotoModule('预约');
await page.locator('.herder_right').getByRole('button').nth(1).click();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('menuitem', { name: '导出图片' }).click();
const download = await downloadPromise;
// 获取下载的文件名
const fileName = download.suggestedFilename();
// 指定保存下载文件的路径
const downloadPath = path.join(__dirname, `@/imgs/${fileName}`);
// 保存下载的文件
await download.saveAs(downloadPath);
// 验证文件是否成功保存
const fileExists = fs.existsSync(downloadPath);
expect(fileExists).toBeTruthy();
});
test('视图调整', async ({ page, homeNavigation, appointmentPage }) => {
const cellBox = { height: 0, lastHeight: 0, width: 0, lastWidth: 0 }; // 单元格
const employee = staffData.firstStore.firstSector.employee_1; // 员工张伟
let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable(); // 预约时间
// 获取当前单元格的宽高
const $time = page.locator('.times_table td', { hasText: setAppointmentTime });
const $employee = page.locator('.room_table .tr .name_th', { hasText: employee.name }).first();
await homeNavigation.gotoModule('预约');
await $time.scrollIntoViewIfNeeded();
await $employee.scrollIntoViewIfNeeded();
let timeBox = await $time.boundingBox();
let employeeBox = await $employee.boundingBox();
cellBox.lastHeight = timeBox != null ? timeBox.height : 0;
cellBox.lastWidth = employeeBox != null ? employeeBox.width : 0;
await page.locator('.herder_right').getByRole('button').nth(1).click();
await page.getByRole('menuitem', { name: '视图调整' }).click();
// 打开了自定义的宽高
await expect(page.locator('.set_w_h_box .set_w_h_title', { hasText: '自定义预约表格宽高' })).toBeVisible();
const setHeightLocator = page.locator('.set_w_h_content', { hasText: '高度' }).getByRole('slider');
const setWidthLocator = page.locator('.set_w_h_content', { hasText: '宽度' }).getByRole('slider');
// 获取可以移动最大和最小的宽度和高度
const heightValueMax = Number(await setHeightLocator.getAttribute('aria-valuemax'));
const heightValueMin = Number(await setHeightLocator.getAttribute('aria-valuemin'));
const widthValueMax = Number(await setWidthLocator.getAttribute('aria-valuemax'));
const widthValueMin = Number(await setWidthLocator.getAttribute('aria-valuemin'));
const heightDistance = heightValueMax - heightValueMin;
const widthDistance = widthValueMax - widthValueMin;
const setHeightBox = await setHeightLocator.boundingBox();
const setWidthBox = await setWidthLocator.boundingBox();
if (setHeightBox == null || setWidthBox == null) {
throw new Error('获取设置高度和宽度的定位器失败');
}
await test.step('设置高度', async () => {
await $time.scrollIntoViewIfNeeded();
// 设置单元格高度
await page.mouse.move(setHeightBox.x + setHeightBox.width / 2, setHeightBox.y + setHeightBox.height / 2);
await page.mouse.down();
await page.mouse.move(
setHeightBox.x + setHeightBox.width / 2 + heightDistance,
setHeightBox.y + setHeightBox.height / 2,
);
await page.mouse.down();
timeBox = await $time.boundingBox();
cellBox.height = timeBox != null ? timeBox.height : 0;
expect(cellBox.height - cellBox.lastHeight).toEqual(heightDistance);
});
await test.step('设置宽度', async () => {
// 设置单元格宽度
await page.mouse.move(setWidthBox.x + setWidthBox.width / 2, setWidthBox.y + setWidthBox.height / 2);
await page.mouse.move(setWidthBox.x, setWidthBox.y);
await page.mouse.down();
await page.mouse.move(
setWidthBox.x + setWidthBox.width / 2 + widthDistance,
setWidthBox.y + setWidthBox.height / 2,
);
await page.mouse.down();
await $employee.scrollIntoViewIfNeeded();
employeeBox = await $employee.boundingBox();
cellBox.width = employeeBox?.width ?? 0;
expect(cellBox.width - cellBox.lastWidth).toEqual(widthDistance);
});
// 恢复默认视图设置
await page.locator('.resetting').click();
});
});
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);
await homeNavigation.gotoModule('预约');
await expect(page.getByText('张伟')).toBeVisible();
const $$userInfo = page.locator('.a_userInfo');
// 获取顾客占用预约单元格信息
const $$appointmentBox = $$userInfo.locator('.appointment_content');
const names = await $$appointmentBox.locator('.name').allInnerTexts();
const userPhones = await $$appointmentBox.locator('.user_phone').allInnerTexts();
const userInfoData = names.map((name, index) => {
return {
name,
phone: userPhones[index],
};
});
// 取消预约占用
// 获取占用框的数量
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();
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);
}
await page.waitForTimeout(500);
occupyBoxCount = await $$userInfo.locator('.occupy').count();
}
// 取消有取消预约按钮的预约
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();
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,276 @@
// @ts-check
import { test, expect } from '@/fixtures/staff_common.js';
import { Employees } from '@/fixtures/userconfig.js';
test.describe('目标-目标设置', () => {
test('门店设置一店', async ({ page, baseURL, numberInput }) => {
const goal = '100'; //目标金额
let verifys; //校验值
const employeeA = Employees.FirstShop.Employee_3.name;
await page.goto(baseURL ?? '');
await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(process.env.staff1_account ?? '');
await page.locator('.pass_svg').waitFor();
await page.getByRole('textbox', { name: '请输入登录密码' }).fill(process.env.staff1_password ?? '');
await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter');
await page.getByRole('button', { name: '同意并登录' }).click();
// await page.getByText('美容顾问 一店顾问').click();
// End of authentication steps.
await expect(page.getByText('收银', { exact: true })).toBeVisible();
await page.waitForLoadState('networkidle');
await page
.locator('div')
.filter({ hasText: /^目标$/ })
.first()
.click();
await page.getByRole('button', { name: '设置目标' }).click();
// 员工A内行数
let nowRowA = 0;
const allTrA = page.locator('.popupComTableStyle .m-table__fixed-left tbody tr');
const countA = await allTrA.count();
// 获取项目处于第几行
for (let i = 0; i < countA; i++) {
const trA = allTrA.nth(i);
const employee = await trA.locator('.popupEmployeeName').first().innerText();
console.log(employee);
if (employee.includes(employeeA)) {
nowRowA = i;
break;
}
}
await page
.locator('.popupComTableStyle .m-table__body .main-table-body_tr')
.nth(nowRowA)
.locator('.m-table-cell')
.nth(1)
.click();
await numberInput.setValue(Number(goal));
await numberInput.confirmValue();
await page.getByRole('button', { name: '保 存' }).click();
// 员工A外行数
let nowRowB = 0;
const allTrB = page.locator('.tableStyle .m-table__fixed-left tbody tr');
const countB = await allTrB.count();
// 获取项目处于第几行
for (let i = 0; i < countB; i++) {
const trB = allTrB.nth(i);
const employee = await trB.locator('.employeeName').first().innerText();
console.log(employee);
if (employee.includes(employeeA)) {
nowRowB = i;
break;
}
}
const verify = await page
.locator('.tableStyle .m-table__body-wrapper tbody tr')
.nth(nowRowB)
.locator('.is-right .targetValue')
.nth(0)
.innerText();
if (verify === '--') {
verifys = 0;
} else {
verifys = verify;
}
// 对比输入的目标
expect.soft(verifys).toBe(goal);
// 清理
await page.getByRole('button', { name: '设置目标' }).click();
// 员工A内行数
for (let i = 0; i < countA; i++) {
const trA = allTrA.nth(i);
const employee = await trA.locator('.popupEmployeeName').first().innerText();
console.log(employee);
if (employee.includes(employeeA)) {
nowRowA = i;
break;
}
}
await page
.locator('.popupComTableStyle .m-table__body .main-table-body_tr')
.nth(nowRowA)
.locator('.m-table-cell')
.nth(1)
.click();
await numberInput.setValue(0);
await numberInput.confirmValue();
await page.getByRole('button', { name: '保 存' }).click();
});
test('门店设置二店', async ({ page, baseURL }) => {
await page.goto(baseURL ?? '');
await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(process.env.staff2_account ?? '');
await page.locator('.pass_svg').waitFor();
await page.getByRole('textbox', { name: '请输入登录密码' }).fill(process.env.staff2_password ?? '');
await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter');
await page.getByRole('button', { name: '同意并登录' }).click();
try {
await page.locator('.versionModal_main_content').waitFor({ timeout: 5000 });
await page.locator('.versionModal_main_content .close_icon').click();
} catch {
console.log('无版本更新');
}
await expect(page.getByText('收银', { exact: true })).toBeVisible();
await page.waitForLoadState('networkidle');
await page
.locator('div')
.filter({ hasText: /^目标$/ })
.first()
.click();
await page.getByRole('button', { name: '设置目标' }).waitFor();
await page.getByRole('button', { name: '设置目标' }).click();
await expect.soft(page.locator('.popup_content', { hasText: '温馨提示' })).toBeVisible();
const WarmReminder = await page.locator('.popup_content .content div').innerText();
console.log(WarmReminder);
expect
.soft(WarmReminder)
.toBe('亲该门店AT测试二店尚未开通目标管理功能您不可为员工设置目标也无法跟进员工目标完成情况。');
await page.getByRole('button', { name: '我知道了' }).click();
});
test('员工设置', async ({ page, numberInput }) => {
// A员工李娜账号密码
const accountA = '139876543';
const passwordA = 'a123456';
// B员工刘强账号密码
const accountB = '134987612';
const passwordB = 'a123456';
const employeeA = Employees.FirstShop.Employee_4.name; // 刘强
const employeeB = Employees.FirstShop.Employee_8.name; // 刘强
const employeeC = Employees.FirstShop.Employee_9.name; // 周萍
const goalB = '100';
const goalC = '200';
let verifyB; //校验B员工
let verifyC; //校验C员工
let verify; //汇总
await page.goto(process.env.BASE_URL ?? '');
await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(accountA);
await page.locator('.pass_svg').waitFor();
await page.getByRole('textbox', { name: '请输入登录密码' }).fill(passwordA);
await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter');
await page.getByRole('button', { name: '同意并登录' }).click();
// await page.getByText('美容顾问 一店顾问').click();
// End of authentication steps.
await expect(page.getByText('收银', { exact: true })).toBeVisible();
await page.waitForLoadState('networkidle');
await page
.locator('div')
.filter({ hasText: /^目标$/ })
.first()
.click();
await page.getByRole('button', { name: '设置目标' }).click();
// 员工B内行数
let nowRowB = 0;
const allTrB = page.locator('.popupComTableStyle .m-table__fixed-left tbody tr');
const countB = await allTrB.count();
// 获取项目处于第几行
for (let i = 0; i < countB; i++) {
const trB = allTrB.nth(i);
const employee = await trB.locator('.popupEmployeeName').first().innerText();
console.log(employee);
if (employee.includes(employeeB)) {
nowRowB = i;
break;
}
}
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.confirmValue();
// 员工C内行数
let nowRowC = 0;
const allTrC = page.locator('.popupComTableStyle .m-table__fixed-left tbody tr');
const countC = await allTrC.count();
// 获取项目处于第几行
for (let i = 0; i < countC; i++) {
const trA = allTrC.nth(i);
const employee = await trA.locator('.popupEmployeeName').first().innerText();
console.log(employee);
if (employee.includes(employeeC)) {
nowRowC = i;
break;
}
}
// 判断李娜在目标设置不存在
await expect(
page
.locator('.popupComTableStyle .m-table__fixed-left tbody tr')
.locator('.popupEmployeeName', { hasText: employeeA }),
).not.toBeVisible();
await code.nth(nowRowC).locator('.m-table-cell').nth(1).click();
await numberInput.setValue(Number(goalC));
await numberInput.confirmValue();
await page.getByRole('button', { name: /保\s存/ }).click();
// 员工B外行数
const allTrBB = page.locator('.tableStyle .m-table__fixed-left tbody tr');
const nowRowBB = await allTrBB.allInnerTexts().then(text => {
return text.findIndex(value => value.includes(employeeB));
});
if (nowRowBB === -1) {
throw new Error('员工B不存在');
}
const codes = page.locator('.tableStyle .m-table__body-wrapper tbody tr');
// 对比输入的目标
await expect(codes.nth(nowRowBB).locator('.is-right .targetValue').nth(0)).toContainText(goalB);
// 员工C外行数
const allTrCC = page.locator('.tableStyle .m-table__fixed-left tbody tr');
const nowRowCC = await allTrCC.allInnerTexts().then(text => {
return text.findIndex(value => value.includes(employeeC));
});
// 对比输入的目标
await expect(codes.nth(nowRowCC).locator('.is-right .targetValue').nth(0)).toContainText(goalC);
// 总数
const allTr = page.locator('.tableStyle .m-table__fixed-left tbody tr');
let nowRow = await allTr.allInnerTexts().then(text => {
return text.findIndex(value => value.includes('目标汇总'));
});
// 对比输入的目标
await expect
.soft(codes.nth(nowRow).locator('.is-right .targetValue').nth(0))
.toContainText(`${Number(goalB) + Number(goalC)}`);
// 退出登录
await page.locator('.exit_btn').click();
await page.locator('.comfirm_btn').click();
await page.goto(process.env.BASE_URL ?? '');
await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(accountB);
await page.locator('.pass_svg').waitFor();
await page.getByRole('textbox', { name: '请输入登录密码' }).fill(passwordB);
await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter');
await page.waitForLoadState('networkidle');
await page
.locator('div')
.filter({ hasText: /^目标$/ })
.first()
.click();
await expect(page.locator('.m-table__fixed-left .m-table-cell', { hasText: '日期' })).toBeVisible();
});
});

View File

@ -0,0 +1,236 @@
import { faker } from '@faker-js/faker/locale/zh_CN';
import { test, expect } from '@/fixtures/staff_common.js';
import { getListIndexForTargetElement } from '@/utils/utils.js';
test.describe('调货管理', () => {
test('门店要货', async ({ page, homeNavigation }) => {
// 使用产品
const goods = {
a: { no: 'aa100007', name: '肌因能量套', unitPrice: 10, quantity: 5 },
b: { no: 'aa100008', name: '肌因赋活尊享套', unitPrice: 15, quantity: 8 },
};
// 备注
const remark = '调货管理-门店要货' + faker.helpers.fromRegExp(/[0-9]{3}/);
let quantity = 0; // 数量
let unitPrice = 0; // 单价
let subtotal = 0; // 小计
let totalQuantity = 0; // 总数量
let totalPrice = 0; // 总价
await test.step('门店保存要货单', async () => {
await homeNavigation.gotoModule('库存');
await page.locator('.top_tab').getByText('调货管理').click();
// 展示单价和小计
await page.locator('.price_set').click();
await page.locator('.set_btn').getByRole('checkbox').check();
await page.getByRole('button', { name: /确\s认/ }).click();
// 选择产品a设置产品数量和单价
await page.locator('.panel tr', { hasText: goods.a.no }).click();
const goodsATr = page.locator('.bill_report tr', { hasText: goods.a.name });
await goodsATr.getByRole('spinbutton').first().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.a.quantity));
await page.locator('.number_tr button').nth(11).click();
await goodsATr.getByRole('spinbutton').last().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.a.unitPrice));
await page.locator('.number_tr button').nth(11).click();
// 选择产品b设置产品数量和单价
await page.locator('.panel tr', { hasText: goods.b.no }).click();
const goodsBTr = page.locator('.bill_report tr', { hasText: goods.b.name });
await goodsBTr.getByRole('spinbutton').first().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.b.quantity));
await page.locator('.number_tr button').nth(11).click();
await goodsBTr.getByRole('spinbutton').last().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.b.unitPrice));
await page.locator('.number_tr button').nth(11).click();
// 设置备注
await page.getByText('备注').click();
await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark);
await page.getByRole('button', { name: /确\s认/ }).click();
// 保存要货单
await page.getByRole('button', { name: /保\s存/ }).click();
await expect(page.getByRole('cell', { name: '门店要货备注' })).toBeInViewport();
});
await test.step('查看保存的要货单', async () => {
// 查看明细单据
const tableTrList = page.locator('.table_inner .main-table-body_tr');
const targetTr = tableTrList.filter({ hasText: remark }).filter({ hasText: '未提交' });
const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList);
const fixedCell = page
.locator('.m-table-fixed-body')
.getByRole('cell', { name: '明细编辑删除' })
.nth(targetIndex);
await fixedCell.getByText('明细').click();
// 拿取商品a的数据
const goodsATr = page.locator('.popup_content tr', { hasText: goods.a.name });
quantity = Number(await goodsATr.locator('td').nth(2).innerText());
unitPrice = Number(await goodsATr.locator('td').nth(6).innerText());
subtotal = Number(await goodsATr.locator('td').nth(7).innerText());
totalPrice += unitPrice * quantity;
totalQuantity += quantity;
// 判断商品a的数据
expect.soft(quantity).toBe(goods.a.quantity);
expect.soft(unitPrice).toBe(goods.a.unitPrice);
expect(subtotal).toBe(goods.a.quantity * goods.a.unitPrice);
// 拿取商品b的数据
const goodsBTr = page.locator('.popup_content tr', { hasText: goods.b.name });
quantity = Number(await goodsBTr.locator('td').nth(2).innerText());
unitPrice = Number(await goodsBTr.locator('td').nth(6).innerText());
subtotal = Number(await goodsBTr.locator('td').nth(7).innerText());
totalPrice += unitPrice * quantity;
totalQuantity += quantity;
// 判断商品b的数据
expect.soft(quantity).toBe(goods.b.quantity);
expect.soft(unitPrice).toBe(goods.b.unitPrice);
expect.soft(subtotal).toBe(goods.b.quantity * goods.b.unitPrice);
// 判断合计数量、合计金额
expect.soft(totalPrice).toBe(goods.b.quantity * goods.b.unitPrice + goods.a.quantity * goods.a.unitPrice);
expect(totalQuantity).toBe(goods.a.quantity + goods.b.quantity);
// 关闭弹窗
await page.locator('.title > .close_icon > svg').click();
});
await test.step('门店提交要货单', async () => {
// 查看明细单据
let tableTrList = page.locator('.table_inner .main-table-body_tr');
let targetTr = tableTrList.filter({ hasText: remark }).filter({ hasText: '未提交' });
let targetIndex = await getListIndexForTargetElement(targetTr, tableTrList);
let fixedCell = page
.locator('.m-table-fixed-body')
.getByRole('cell', { name: '明细编辑删除' })
.nth(targetIndex);
await fixedCell.getByText('编辑').click();
await expect(page.getByText('要货单').nth(1)).toBeInViewport();
// 选择产品a设置产品数量和单价
await page.locator('.panel tr', { hasText: goods.a.no }).click();
let goodsTr = page.locator('.bill_report tr', { hasText: goods.a.name });
await goodsTr.getByRole('spinbutton').first().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.a.quantity * 2));
await page.locator('.number_tr button').nth(11).click();
await goodsTr.getByRole('spinbutton').last().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.a.unitPrice * 2));
await page.locator('.number_tr button').nth(11).click();
// 选择产品b设置产品数量和单价
await page.locator('.panel tr', { hasText: goods.b.no }).click();
goodsTr = page.locator('.bill_report tr', { hasText: goods.b.name });
await goodsTr.getByRole('spinbutton').first().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.b.quantity * 2));
await page.locator('.number_tr button').nth(11).click();
await goodsTr.getByRole('spinbutton').last().click();
await page.getByPlaceholder('请输入内容').fill(String(goods.b.unitPrice * 2));
await page.locator('.number_tr button').nth(11).click();
// 保存要货单
await page.getByRole('button', { name: /提\s交/ }).click();
await expect(page.getByRole('cell', { name: '门店要货备注' })).toBeInViewport();
});
await test.step('查看提交的要货单', async () => {
// 查看明细单据
const tableTrList = page.locator('.table_inner .main-table-body_tr');
const targetTr = tableTrList.filter({ hasText: remark }).filter({ hasText: '已提交' });
const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList);
const fixedCell = page.locator('.m-table-fixed-body').getByRole('cell', { name: '明细' }).nth(targetIndex);
await fixedCell.getByText('明细').click();
totalQuantity = 0;
totalPrice = 0;
// 拿取商品a的数据
let goodsTr = page.locator('.popup_content tr', { hasText: goods.a.name });
quantity = Number(await goodsTr.locator('td').nth(2).innerText());
unitPrice = Number(await goodsTr.locator('td').nth(6).innerText());
subtotal = Number(await goodsTr.locator('td').nth(7).innerText());
totalPrice += unitPrice * quantity;
totalQuantity += quantity;
// 判断商品a的数据
expect.soft(quantity).toBe(goods.a.quantity * 2);
expect.soft(unitPrice).toBe(goods.a.unitPrice * 2);
expect.soft(subtotal).toBe(goods.a.quantity * goods.a.unitPrice * 4);
// 拿取商品b的数据
goodsTr = page.locator('.popup_content tr', { hasText: goods.b.name });
quantity = Number(await goodsTr.locator('td').nth(2).innerText());
unitPrice = Number(await goodsTr.locator('td').nth(6).innerText());
subtotal = Number(await goodsTr.locator('td').nth(7).innerText());
totalPrice += unitPrice * quantity;
totalQuantity += quantity;
// 判断商品b的数据
expect.soft(quantity).toBe(goods.b.quantity * 2);
expect.soft(unitPrice).toBe(goods.b.unitPrice * 2);
expect.soft(subtotal).toBe(goods.b.quantity * goods.b.unitPrice * 4);
// 判断合计数量、合计金额
expect
.soft(totalPrice)
.toBe((goods.b.quantity * goods.b.unitPrice + goods.a.quantity * goods.a.unitPrice) * 4);
expect(totalQuantity).toBe((goods.a.quantity + goods.b.quantity) * 2);
});
});
test('要货单', async ({ firstStaffPage, staffHomeNavigation, transferManagementPage }) => {
const goods = { no: 'aa100024', name: '茉莉精油', quantity: 10 };
const remark = '门店要货单' + faker.string.numeric(3);
await test.step('添加要货单', async () => {
await staffHomeNavigation.gotoModule('库存');
await firstStaffPage.locator('.top_tab .tab_item').getByText('调货管理').click();
await transferManagementPage.gotoSubPage('门店要货');
await firstStaffPage
.getByRole('row', { name: goods.no })
.and(firstStaffPage.getByRole('row', { name: goods.name }))
.click();
const goodsTr = firstStaffPage.locator('.bill_report tr', { hasText: goods.name });
await goodsTr.getByRole('spinbutton').first().click();
await firstStaffPage.getByPlaceholder('请输入内容').fill(String(goods.quantity));
await firstStaffPage.locator('.number_tr button').nth(11).click();
// 设置备注
await firstStaffPage.getByText('备注').click();
await firstStaffPage.getByPlaceholder('请输入1-100个字符备注内容').fill(remark);
await firstStaffPage.getByRole('button', { name: /确\s认/ }).click();
// 保存要货单
await firstStaffPage.getByRole('button', { name: /保\s存/ }).click();
// 跳转到要货单
await expect(firstStaffPage.getByRole('cell', { name: '门店要货备注' })).toBeVisible();
const billGoodsTr = firstStaffPage.locator('.m-table__body-wrapper tr', {
hasText: remark,
});
await expect(billGoodsTr).toBeVisible();
});
await test.step('删除要货单', async () => {
const tableTrList = firstStaffPage.locator('.table_inner .main-table-body_tr');
const targetTr = tableTrList.filter({ hasText: remark });
const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList);
const fixedCell = firstStaffPage
.locator('.m-table-fixed-body')
.getByRole('cell', { name: '明细编辑删除' })
.nth(targetIndex);
await fixedCell.getByText('删除').click();
await firstStaffPage.getByRole('button', { name: /确\s认/ }).click();
const billGoodsTr = firstStaffPage.locator('.m-table__body-wrapper tr', {
hasText: remark,
});
await expect(billGoodsTr).not.toBeInViewport();
});
});
});

40
tests/utils/customer.ts Normal file
View File

@ -0,0 +1,40 @@
import { faker } from '@faker-js/faker/locale/zh_CN';
export type employee = { level: string, name: string };
interface CustomerOptions {
username?: string;
phone?: string;
archive?: string;
gender?: number;
source?: number;
birthday?: { year: number; month: number; day: number };
remark?: string;
employees?: employee[];
}
export class Customer {
store: number;
department: number;
username: string;
phone: string;
archive: string;
gender: number;
birthday?: { year: number; month: number; day: number };
source: number;
remark: string;
employees: employee[];
constructor(store: number, department: number, options: CustomerOptions = {}) {
this.store = store;
this.department = department;
this.username = options.username ?? faker.person.fullName();
this.phone = options.phone ?? faker.helpers.fromRegExp(/1[3-9][0-9]{6}/);
this.archive = options.archive ?? '';
this.gender = options.gender ?? 0;
this.source = options.source ?? 1;
this.birthday = options.birthday;
this.remark = options.remark ?? '';
this.employees = options.employees ?? [];
}
}

View File

@ -0,0 +1,132 @@
export async function readIndexedDB() {
const databases = await indexedDB.databases();
const allData: Array<{ databaseName: string; data: { key: IDBValidKey; value: any }[] }> = [];
for (const db of databases) {
const databaseName = db.name!;
try {
const dbInstance = await openDatabase(databaseName);
const transaction = dbInstance.transaction(dbInstance.objectStoreNames, 'readonly');
const objectStore = transaction.objectStore(dbInstance.objectStoreNames[0]);
const storeData = await getAllDataFromObjectStore(objectStore);
allData.push({
databaseName,
data: storeData,
});
dbInstance.close();
} catch (error) {
console.error(`Error reading database ${databaseName}:`, error);
}
}
return allData;
}
function openDatabase(databaseName: string): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(databaseName);
request.onsuccess = event => resolve(event.target.result);
request.onerror = event => reject(event.target.error);
});
}
function getAllDataFromObjectStore(objectStore: IDBObjectStore): Promise<Array<{ key: IDBValidKey; value: any }>> {
return new Promise((resolve, reject) => {
const storeData: Array<{ key: IDBValidKey; value: any }> = [];
const cursorRequest = objectStore.openCursor();
cursorRequest.onsuccess = event => {
const cursor = event.target.result;
if (cursor) {
storeData.push({ key: cursor.key, value: cursor.value });
cursor.continue();
} else {
resolve(storeData);
}
};
cursorRequest.onerror = event => reject(event.target.error);
});
}
export async function writeIndexedDB(jsonData) {
return new Promise((resolve, reject) => {
const processDatabase = async (database, objectStoreNames) => {
try {
// 动态获取数据库版本号
const version = database.version || 2;
const openRequest = indexedDB.open(database.databaseName, version);
// 处理数据库版本升级
openRequest.onupgradeneeded = event => {
const db = event.target.result;
objectStoreNames.forEach(storeName => {
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName);
}
});
};
// 处理数据库打开成功
openRequest.onsuccess = async event => {
const db = event.target.result;
const table = database.data;
const transaction = db.transaction(objectStoreNames, 'readwrite');
const objectStore = transaction.objectStore(objectStoreNames[0]);
// 添加事务错误处理
transaction.onerror = event => {
console.error('事务失败:', event.target.error);
reject('事务失败');
};
let completedOperations = 0;
const totalOperations = table.length;
// 使用 Promise.all 来确保所有的写入操作完成
const writeOperations = table.map((item, index) => {
return new Promise((innerResolve, innerReject) => {
const addObject = objectStore.put(item.value, item.key);
addObject.onsuccess = () => {
completedOperations++;
if (completedOperations === totalOperations) {
innerResolve('数据写入成功');
}
};
addObject.onerror = event => {
console.error('数据添加失败:', event.target.error);
innerReject('数据添加失败');
};
});
});
// 等待所有写入操作完成
try {
await Promise.all(writeOperations);
resolve('所有数据写入完成');
} catch (error) {
reject('数据写入失败');
}
};
// 错误处理
openRequest.onerror = event => {
console.error('数据库打开失败:', event.target.error);
reject('数据库打开失败');
};
} catch (error) {
reject('数据库操作失败: ' + error);
}
};
// 遍历所有数据库
const objectStoreNames = ['keyvaluepairs', 'local-forage-detect-blob-support'];
Promise.all(jsonData.map(database => processDatabase(database, objectStoreNames)))
.then(() => resolve('所有数据库操作完成'))
.catch(error => reject('操作失败: ' + error));
});
}

201
tests/utils/utils.js Normal file
View File

@ -0,0 +1,201 @@
// 解析二维码
const decodeImage = require('jimp').read;
const { readFile, unlinkSync } = require('fs');
const qrcodeReader = require('qrcode-reader');
const sharp = require('sharp');
const Tesseract = require('tesseract.js');
/**
* 解析二维码
* @param {*} pathSrc 图片路径
* @returns Promise<qrResult> 二维码内容
*/
export const decodeQR = function(pathSrc) {
// var filePath = path.resolve(__dirname, pathSrc);
return new Promise((resolve, reject) => {
readFile(pathSrc, function(err, fileBuffer) {
if (err) {
reject(err);
return;
}
decodeImage(fileBuffer, function(err, image) {
if (err) {
reject(err);
return;
}
let decodeQR = new qrcodeReader();
decodeQR.callback = function(errorWhenDecodeQR, result) {
if (errorWhenDecodeQR) {
reject(errorWhenDecodeQR);
unlinkSync(pathSrc);
return;
}
if (!result) {
console.log('gone with wind');
resolve('');
unlinkSync(pathSrc);
} else {
resolve(result.result);
console.log(result.result); //结果
unlinkSync(pathSrc);
}
};
decodeQR.decode(image.bitmap);
});
});
});
};
/**
* - ¥1500 -> { method: '', amount: 1500 }
* - 银联¥1500.0 -> { method: 银联, amount: 1500 }
* @param {string} amountText ¥1500
* @returns {{ method: string, amount: number }}
*/
export const convertAmountText = function(amountText) {
let method = '';
let amount = 0;
const text = amountText.replace('\n', '');
const amountTextArray = text.match(/(\s)¥([0-9.]+)/);
if (amountTextArray) {
method = amountTextArray[1];
amount = Number(amountTextArray[2]);
}
return { method: method, amount: amount };
};
/**
* 返回列表中目标元素的index
* @param {import('@playwright/test').Locator} targetElement
* @param elementList
*/
export const getListIndexForTargetElement = async (targetElement, elementList) => {
return targetElement.evaluate((el, list) => {
return Array.from(list).indexOf(el);
}, await elementList.elementHandles());
};
/**
* 保留数字(清空非数字)
* @param {*} str 仅保留数字
* @returns
*/
export function KeepOnlyNumbers(str) {
return str.replace(/\D/g, '');
}
/**
* 保留中文和数字(清空符号)
* @param {*} str 仅保留中文和数字
* @returns
*/
export function CleanPunctuation(str) {
return str.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '');
}
/**
* 等待指定接口加载完成
* @param {import('@playwright/test').Page} page
* @param {string[]} apiArray 接口名称数组
* @returns Promise<Response[]>
*/
export const waitSpecifyApiLoad = (page, apiArray) => {
if (apiArray === undefined || apiArray.length === 0) {
return Promise.resolve([]);
}
return Promise.all(
apiArray.map(api => page.waitForResponse(res => res.url().includes(api) && res.status() === 200)),
);
};
/**
* 等待Locator元素内的文本值不再变化
* @param {import('@playwright/test').Locator} locator
* @param {boolean} [waitForAnimations = false] 等待动画结束
* @param {number} [sameTextCount = 20] 重复次数
* @returns
*/
export const waitStable = async function(locator, waitForAnimations = false, sameTextCount = 20) {
await locator.evaluate(
async (element, { waitForAnimations, sameTextCount }) => {
const progressIsStable = async function(element, lastText = null, sameTextCounter = 0, iteration = 0) {
// 递归次数超过500次退出
if (iteration > 500) {
throw new Error('超出了最大的递归次数');
}
// 等待动画结束
if (waitForAnimations) {
await Promise.all(element.getAnimations().map(animation => animation.finished));
}
// 间隔15ms检测一次元素变化
await new Promise(resolve => setTimeout(resolve, 15));
// 获取当前元素文本
const text = element.innerText;
// 和上次元素的文本进行对比
const sameText = text === lastText;
if (sameText) {
// 和上次一致,则+1
++sameTextCounter;
} else {
// 和上次不一致则归0
sameTextCounter = 0;
}
const isStable = sameTextCounter >= sameTextCount;
if (isStable) {
// 重复次数超过20次则推出循环
return true;
} else {
// 否则继续递归
return progressIsStable(element, text, sameTextCounter, ++iteration);
}
};
// 进行递归
return progressIsStable(element);
},
{ waitForAnimations, sameTextCount },
);
// 返回通过校验的locator
return locator;
};
/**
* 处理图像并进行验证码识别
* @param {string} inputImagePath - 输入图像文件名位于 .images 文件夹内
* @param {string} outputImagePath - 输出处理后图像的文件名位于 .images 文件夹内
* @returns {Promise<string>} - 返回识别的文本结果
*/
export const processAndRecognizeCaptcha = async (inputImagePath, outputImagePath) => {
try {
// 图像处理
await sharp(inputImagePath)
// .sharpen()
.modulate({
brightness: 1.2, // 增加亮度
contrast: 1.5, // 增强对比度
})
.resize(800) // 调整图像宽度为800像素保持纵横比
.grayscale() // 转换为灰度图
.threshold(128) // 二值化阈值设定为128
.toFile(outputImagePath);
console.log('图像处理完成:', outputImagePath);
// 图像识别
const {
data: { text },
} = await Tesseract.recognize(outputImagePath, 'eng', {
langPath: './tessdata',
});
console.log('识别结果:', text.trim());
return text.replace(/\s+/g, '').trim(); // 返回识别的文本结果
} catch (err) {
console.error('处理或识别出错:', err);
throw err; // 将错误抛出以供调用者处理
}
};

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022",
"jsx": "react",
"strictNullChecks": true,
"strictFunctionTypes": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*":["tests/*"]
}
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}