This commit is contained in:
commit
db0603dcfd
16
.env
Normal file
16
.env
Normal file
@ -0,0 +1,16 @@
|
||||
BASE_URL="https://hlk.meiguanjia.net/#/"
|
||||
H5_BASE_URL="https://hlk.meiguanjia.net/h5/#/?env=0&mid=464&tv=qym&query=%257B%2522tab%2522%253A%2522MODULE_202311200022011%2522%257D"
|
||||
ACCOUNT=15500005555
|
||||
PASSWORD=hlk123
|
||||
SMSCODE=1660
|
||||
|
||||
ZHB_ACCOUNT=18571458300
|
||||
ZHB_PASSWORD=1
|
||||
ZHB_ADMIN_URL="https://shengyibao.meiguanjia.net/shair/components/shair-web/base-system/index.html#"
|
||||
ZHB_BASE_URL="https://shengyibao.meiguanjia.net/young/"
|
||||
ZHB_H5_URL="http://shengyibao.meiguanjia.net/h5/index.html#tenantId=1578151&view=0"
|
||||
|
||||
MGJ_ACCOUNT=美管加演示
|
||||
MGJ_PASSWORD=1
|
||||
# MGJ_BASE_URL="https://vip1.meiguanjia.net/shair/?v=mgj"
|
||||
MGJ_BASE_URL="https://young.meiguanjia.net/young/"
|
||||
30
.github/workflows/playwright.yml
vendored
Normal file
30
.github/workflows/playwright.yml
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
schedule:
|
||||
- cron: '30 9 * * *' # 每天凌晨 12 点执行一次
|
||||
workflow_dispatch: # 手动触发
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'v20.17'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps
|
||||
- name: Run tests repeat 3
|
||||
run: npm run test:repeat
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.auth/
|
||||
.images/
|
||||
*.traineddata
|
||||
/tessdata/**/*.traineddata
|
||||
|
||||
138
package-lock.json
generated
Normal file
138
package-lock.json
generated
Normal file
@ -0,0 +1,138 @@
|
||||
{
|
||||
"name": "playwright-demo",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-demo",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"csv-parse": "^5.5.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.0.3",
|
||||
"@playwright/test": "^1.48.1",
|
||||
"@types/node": "^22.7.5",
|
||||
"dotenv": "^16.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@faker-js/faker/-/faker-9.0.3.tgz",
|
||||
"integrity": "sha512-lWrrK4QNlFSU+13PL9jMbMKLJYXDFu3tQfayBsMXX7KL/GiQeqfB1CzHkqD5UHBUtPAuPo6XwGbMFNdVMZObRA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.48.1.tgz",
|
||||
"integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.48.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.7.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.7.5.tgz",
|
||||
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csv-parse": {
|
||||
"version": "5.5.6",
|
||||
"resolved": "https://registry.npmmirror.com/csv-parse/-/csv-parse-5.5.6.tgz",
|
||||
"integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.48.1.tgz",
|
||||
"integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.48.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.48.1",
|
||||
"resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.48.1.tgz",
|
||||
"integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "playwright-demo",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"hlk_codegen": "npx playwright codegen https://hlk.meiguanjia.net/#/",
|
||||
"mgj_codegen": "npx playwright codegen https://vip1.meiguanjia.net/shair/?v=mgj",
|
||||
"zhb_codegen": "npx playwright codegen https://shengyibao.meiguanjia.net/young/",
|
||||
"test:repeat": "npx playwright test ./tests/zhb ./tests/mgj ./tests/hlk --repeat-each=3",
|
||||
"ui": "npx playwright test --ui",
|
||||
"pwi": "npm ci && npx playwright install",
|
||||
"pwu": "npx playwright install --with-deps"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.0.3",
|
||||
"@playwright/test": "^1.48.1",
|
||||
"@types/node": "^22.7.5",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"csv-parse": "^5.5.6"
|
||||
}
|
||||
}
|
||||
90
playwright.config.js
Normal file
90
playwright.config.js
Normal file
@ -0,0 +1,90 @@
|
||||
// @ts-check
|
||||
const { defineConfig, devices } = require('@playwright/test');
|
||||
const dotenv = require('dotenv');
|
||||
const path = require('path');
|
||||
|
||||
// Read from ".env" file.
|
||||
dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
const hlkAuthFile = path.join(__dirname, '.auth/hlk_admin.json');
|
||||
const mgjAuthFile = path.join(__dirname, '.auth/mgj_admin.json');
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
module.exports = defineConfig({
|
||||
testDir: './tests',
|
||||
/* 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 ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : 1,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.BASE_URL,
|
||||
timezoneId: 'Asia/Shanghai',
|
||||
locale: 'zh-CN',
|
||||
geolocation: { longitude: 114.24, latitude: 22.73 },
|
||||
permissions: ['geolocation'],
|
||||
extraHTTPHeaders: {
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
},
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{ name: 'hlk_setup', use: { baseURL: process.env.BASE_URL }, testMatch: /hlk\.setup\.js/ },
|
||||
{ name: 'zhb_setup', use: { baseURL: process.env.ZHB_BASE_URL }, testMatch: /zhb\.setup\.js/ },
|
||||
{ name: 'mgj_setup', use: { baseURL: process.env.MGJ_BASE_URL }, testMatch: /mgj\.setup\.js/ },
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL,
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: hlkAuthFile,
|
||||
},
|
||||
testMatch: '**/tests/hlk/**.spec.js',
|
||||
dependencies: ['hlk_setup'],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
baseURL: process.env.ZHB_BASE_URL,
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
testMatch: '**/tests/zhb/**.spec.js',
|
||||
dependencies: ['zhb_setup'],
|
||||
},
|
||||
{
|
||||
name: 'chromium',
|
||||
use: {
|
||||
baseURL: process.env.MGJ_BASE_URL,
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: mgjAuthFile,
|
||||
},
|
||||
testMatch: '**/tests/mgj/**.spec.js',
|
||||
dependencies: ['mgj_setup'],
|
||||
},
|
||||
],
|
||||
|
||||
/* 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,
|
||||
// },
|
||||
});
|
||||
449
tests-examples/demo-todo-app.spec.js
Normal file
449
tests-examples/demo-todo-app.spec.js
Normal 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);
|
||||
}
|
||||
60
tests/hlk/demo.spec.js
Normal file
60
tests/hlk/demo.spec.js
Normal file
@ -0,0 +1,60 @@
|
||||
const { test, expect } = require('./fixtures/common');
|
||||
const { Customer } = require('./pom/customer');
|
||||
const { faker } = require('@faker-js/faker/locale/zh_CN');
|
||||
|
||||
test('登录touch和h5,创建顾客购买会员卡,使用顾客账号到h5商城购买货品', async ({
|
||||
touchPage,
|
||||
h5Page,
|
||||
touchCustomerPage,
|
||||
h5LoginPage,
|
||||
}) => {
|
||||
const phone = faker.helpers.fromRegExp(/1[3-9][0-1]{9}/);
|
||||
const customer = new Customer(1, 1, { phone: phone });
|
||||
|
||||
await touchCustomerPage.createCustomer(customer);
|
||||
/** @type string*/
|
||||
let cardName;
|
||||
await test.step('在touch页面购买会员卡', async () => {
|
||||
await touchPage.getByText('去开单').click();
|
||||
await touchPage.locator('.more > .icon > svg').click();
|
||||
await touchPage.getByText('去开卡').click();
|
||||
const $firstCard = touchPage.locator('.memberCard_box').first();
|
||||
await $firstCard.click();
|
||||
cardName = await $firstCard.locator('.card_name').innerText();
|
||||
await touchPage.getByRole('button', { name: '去结算' }).click();
|
||||
await touchPage.locator('.row').filter({ hasText: '总额' }).locator('.touchIcon').click();
|
||||
await touchPage.getByPlaceholder('请输入内容').fill('1000');
|
||||
await touchPage.locator('div').filter({ hasText: /^789$/ }).getByRole('button').nth(3).click();
|
||||
await touchPage.locator('.paymentInfoItem', { hasText: '现金' }).click();
|
||||
await touchPage.getByText('推送消费提醒').click();
|
||||
await touchPage.getByLabel('结算签字').uncheck();
|
||||
await touchPage.getByRole('button', { name: /结\s算/ }).click();
|
||||
await touchPage.getByRole('button', { name: /跳\s过/ }).click();
|
||||
});
|
||||
|
||||
await test.step('登录h5,并且商城页面使用会员卡进行购买', async () => {
|
||||
await h5LoginPage.login(customer.phone);
|
||||
await h5Page.locator('.singIn_content > .iconfont').click();
|
||||
const element = h5Page.locator('.coupon_content');
|
||||
const boundingBox = await element.boundingBox();
|
||||
|
||||
if (boundingBox) {
|
||||
const x = boundingBox.x + boundingBox.width / 2; // 元素水平中心
|
||||
const y = boundingBox.y - 10; // 元素上侧空白区域
|
||||
|
||||
await h5Page.mouse.click(x, y);
|
||||
}
|
||||
await h5Page.locator('.bar_item', { hasText: '商城' }).click();
|
||||
await expect(h5Page.locator('.title', { hasText: '商城' })).toBeVisible();
|
||||
await h5Page.locator('.li span', { hasText: '全部' }).click();
|
||||
await h5Page.locator('.p-item').nth(2).click();
|
||||
await h5Page.getByText('立即购买').click();
|
||||
await h5Page.locator('uni-text').filter({ hasText: '佘山二店' }).click();
|
||||
await h5Page.getByText('确认').click();
|
||||
await h5Page.getByText('哎哟代理卡').click();
|
||||
await expect(h5Page.getByText('确认支付')).toBeEnabled();
|
||||
await h5Page.getByText('确认支付').click();
|
||||
await expect(h5Page.getByText('支付成功').first()).toBeVisible();
|
||||
await expect(h5Page.getByText('查看订单')).toBeVisible();
|
||||
});
|
||||
});
|
||||
38
tests/hlk/fixtures/base.js
Normal file
38
tests/hlk/fixtures/base.js
Normal file
@ -0,0 +1,38 @@
|
||||
const { test: base, expect, devices } = require('@playwright/test');
|
||||
const authFile = '.auth/hlk_admin.json';
|
||||
|
||||
const test = base.extend({
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
touchPage: async ({ browser, baseURL }, use) => {
|
||||
const context = await browser.newContext({
|
||||
storageState: authFile,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(baseURL);
|
||||
|
||||
await use(page);
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
},
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
h5Page: async ({ browser }, use) => {
|
||||
const iPhone = devices['iPhone 11'];
|
||||
const context = await browser.newContext({
|
||||
...iPhone,
|
||||
storageState: undefined,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(process.env.H5_BASE_URL);
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
test,
|
||||
expect,
|
||||
};
|
||||
7
tests/hlk/fixtures/common.js
Normal file
7
tests/hlk/fixtures/common.js
Normal file
@ -0,0 +1,7 @@
|
||||
const { mergeTests } = require('@playwright/test');
|
||||
const { test: touchTest } = require('./touchFixture.js');
|
||||
const { test: h5Test } = require('./h5Fixture.js');
|
||||
|
||||
export const test = mergeTests(touchTest, h5Test);
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
14
tests/hlk/fixtures/h5Fixture.js
Normal file
14
tests/hlk/fixtures/h5Fixture.js
Normal file
@ -0,0 +1,14 @@
|
||||
const { test: base, expect } = require('./base');
|
||||
const { H5LoginPage } = require('../pom/h5LoginPage');
|
||||
|
||||
const test = base.extend({
|
||||
/**
|
||||
* @type {H5LoginPage}
|
||||
*/
|
||||
h5LoginPage: async ({ h5Page }, use) => {
|
||||
const h5LoginPage = new H5LoginPage(h5Page);
|
||||
await use(h5LoginPage);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = { test, expect };
|
||||
14
tests/hlk/fixtures/touchFixture.js
Normal file
14
tests/hlk/fixtures/touchFixture.js
Normal file
@ -0,0 +1,14 @@
|
||||
const { test: base, expect } = require('./base');
|
||||
const { CustomerPage } = require('../pom/customerPage');
|
||||
|
||||
const test = base.extend({
|
||||
/**
|
||||
* @type {CustomerPage}
|
||||
*/
|
||||
touchCustomerPage: async ({ touchPage }, use) => {
|
||||
const customerPage = new CustomerPage(touchPage);
|
||||
await use(customerPage);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = { test, expect };
|
||||
48
tests/hlk/pom/customer.js
Normal file
48
tests/hlk/pom/customer.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { faker } from '@faker-js/faker/locale/zh_CN';
|
||||
|
||||
export class Customer {
|
||||
/**
|
||||
* @param {1 | 2} store - 门店
|
||||
* @param {1 | 2} department - 部门
|
||||
* @param {string} [username=faker.person.fullName()] - 用户名
|
||||
* @param {string} [phone=faker.helpers.fromRegExp(/1[3-9][0-9]{6}/)] - 手机号
|
||||
* @param {string} [archive=""] - 档案号
|
||||
* @param {number} [gender=0] - 性别
|
||||
* @param {number} [source=1] - 顾客来源
|
||||
* @param {Object} [birthday=undefined] - 生日
|
||||
* @param {number} [birthday.year] - 年
|
||||
* @param {number} [birthday.month] - 月
|
||||
* @param {number} [birthday.day] - 月
|
||||
* @param {string} [remark=undefined]
|
||||
* @param {Array} [employee=undefined]
|
||||
* @constructor
|
||||
* @example
|
||||
* const customer = new Client(1, 2, { username: '22' });
|
||||
* const customer = new Customer(1, 2);
|
||||
*/
|
||||
constructor(
|
||||
store,
|
||||
department,
|
||||
{
|
||||
username = faker.person.fullName(),
|
||||
phone = faker.helpers.fromRegExp(/1[3-9][0-9]{6}/),
|
||||
archive = '',
|
||||
gender = 0,
|
||||
source = 1,
|
||||
birthday = undefined,
|
||||
remark = undefined,
|
||||
employee = undefined,
|
||||
} = {}
|
||||
) {
|
||||
this.store = store;
|
||||
this.department = department;
|
||||
this.username = username;
|
||||
this.phone = phone;
|
||||
this.archive = archive;
|
||||
this.gender = gender;
|
||||
this.source = source;
|
||||
this.birthday = birthday;
|
||||
this.remark = remark;
|
||||
this.employee = employee;
|
||||
}
|
||||
}
|
||||
326
tests/hlk/pom/customerPage.js
Normal file
326
tests/hlk/pom/customerPage.js
Normal file
@ -0,0 +1,326 @@
|
||||
//@ts-check
|
||||
const { expect } = require('@playwright/test');
|
||||
const { Customer } = require('./customer');
|
||||
|
||||
class CustomerPage {
|
||||
/**
|
||||
* @param {import("@playwright/test").Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.$module = this.page.locator('.left_box .link_item').filter({ hasText: /顾客/ });
|
||||
this.$tabItem = this.page.locator('.top_tab .tab_item');
|
||||
this.$summary = this.$tabItem.filter({ hasText: '顾客概要' });
|
||||
this.$distribution = this.$tabItem.filter({ hasText: '顾客分配' });
|
||||
this.$dynamic = this.$tabItem.filter({ hasText: '顾客动态' });
|
||||
this.$analysis = this.$tabItem.filter({ hasText: '顾客分析' });
|
||||
this.$serviceLog = this.$tabItem.filter({ hasText: '服务日志' });
|
||||
this.$register = this.page.locator('.regmeber_warp', { hasText: '创建会员' });
|
||||
this.firstStore = {
|
||||
firstDepartment: { no: 1, name: '美容部' },
|
||||
secondDepartment: { no: 2, name: '医美部' },
|
||||
};
|
||||
this.secondStore = {
|
||||
firstDepartment: { no: 1, name: '美容部' },
|
||||
};
|
||||
this.source = [
|
||||
'邀客',
|
||||
'员工带客',
|
||||
'美团',
|
||||
'大众点评',
|
||||
'客带客',
|
||||
'上门客人',
|
||||
'百度糯米',
|
||||
'支付宝',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转子页面,并且等待页面接口加载完成
|
||||
* @param {import("@playwright/test").Locator} locator
|
||||
* @param {string[]} apiList
|
||||
*/
|
||||
gotoSubPage = async (locator, apiList) => {
|
||||
await expect(async () => {
|
||||
if (!(await locator.getAttribute('class'))?.includes('active')) {
|
||||
await locator.click();
|
||||
}
|
||||
await expect(locator).toHaveClass(/active/);
|
||||
}).toPass({ timeout: 30000 });
|
||||
|
||||
await Promise.all(
|
||||
apiList.map((api) =>
|
||||
this.page.waitForResponse((res) => res.url().includes(api) && res.status() === 200)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 跳转到顾客概要
|
||||
*/
|
||||
gotoSummary = async () => {
|
||||
await this.gotoSubPage(this.$summary, ['summary', 'todo']);
|
||||
};
|
||||
|
||||
/**
|
||||
* 跳转到顾客分配
|
||||
*/
|
||||
gotoDistribution = async () => {
|
||||
await this.gotoSubPage(this.$distribution, ['search_new', 'distribution']);
|
||||
};
|
||||
|
||||
/**
|
||||
* 跳转到顾客动态
|
||||
*/
|
||||
gotoDynamic = async () => {
|
||||
await this.gotoSubPage(this.$dynamic, ['daily_action']);
|
||||
};
|
||||
|
||||
/**
|
||||
* 跳转到顾客分析
|
||||
*/
|
||||
gotoAnalysis = async () => {
|
||||
await this.gotoSubPage(this.$analysis, ['analysis']);
|
||||
};
|
||||
|
||||
/**
|
||||
* 跳转到服务日志
|
||||
*/
|
||||
gotoServiceLog = async () => {
|
||||
await this.gotoSubPage(this.$serviceLog, ['service_log']);
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索顾客
|
||||
* @param {Customer} customer
|
||||
*/
|
||||
searchCustomer = async (customer) => {
|
||||
const searchLocator = this.page.locator('.search_normal');
|
||||
const searchInput = this.page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索');
|
||||
await searchInput.fill(customer.phone);
|
||||
await searchLocator.filter({ has: searchInput }).getByText('搜索', { exact: true }).click();
|
||||
const customerLocator = this.page
|
||||
.locator('.member_list .member_list_li')
|
||||
.filter({ hasText: customer.username });
|
||||
await customerLocator.click();
|
||||
await expect(customerLocator).not.toBeVisible();
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开顾客详情页面
|
||||
* @param {Customer} customer
|
||||
*/
|
||||
openCustomerDetails = async (customer) => {
|
||||
const $username = this.page.getByText(customer.username).last();
|
||||
await $username.click();
|
||||
const infoBox = this.page.locator('.member_info_box');
|
||||
|
||||
await Promise.all([
|
||||
expect(infoBox.getByText(customer.username)).toBeVisible(),
|
||||
this.page.waitForFunction(() => {
|
||||
return document.readyState === 'complete';
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭顾客详情页面
|
||||
*/
|
||||
closeCustomerDetails = async () => {
|
||||
const closeButton = this.page.locator('.member_info_box .close_icons > svg');
|
||||
await closeButton.click();
|
||||
await expect(closeButton).not.toBeVisible();
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建顾客
|
||||
* @param {Customer} customer 顾客
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
createCustomer = async (customer) => {
|
||||
await expect(async () => {
|
||||
await this.$module.click({ clickCount: 1 });
|
||||
await expect(this.$module).toHaveClass(/active/, { timeout: 2000 });
|
||||
}).toPass({ timeout: 30000 });
|
||||
|
||||
// 选择门店
|
||||
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(customer.store - 1)
|
||||
.click();
|
||||
await this.page.getByRole('button', { name: /保.*存/ }).click();
|
||||
|
||||
await this.page.getByRole('button', { name: '新增顾客' }).click();
|
||||
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);
|
||||
}
|
||||
|
||||
// 选择部门
|
||||
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}不存在`);
|
||||
}
|
||||
|
||||
// 选择员工
|
||||
|
||||
// 选择性别
|
||||
if (customer.gender) {
|
||||
if (customer.gender === 0) {
|
||||
await this.$register.locator('label').filter({ hasText: '女性' }).click();
|
||||
} else if (customer.gender === 1) {
|
||||
await this.$register.locator('label').filter({ hasText: '男性' }).click();
|
||||
}
|
||||
}
|
||||
|
||||
// 选择生日
|
||||
const birthday = customer.birthday;
|
||||
const birthdayLocator = this.$register.locator('.ant-form-item', { hasText: '生日' });
|
||||
if (birthday) {
|
||||
const { year, month, day } = birthday;
|
||||
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}其中一个为空`);
|
||||
}
|
||||
}
|
||||
|
||||
// 选择顾客来源
|
||||
// await this.$register.getByLabel(this.source[customer.source]).click();
|
||||
|
||||
await this.$register.getByText('请选择顾客来源').click();
|
||||
await this.page.getByRole('option', { name: this.source[customer.source] }).first().click();
|
||||
|
||||
// 选择备注
|
||||
if (customer.remark) {
|
||||
await this.$register.getByPlaceholder('请输入1-100个字符备注内容').fill(customer.remark);
|
||||
}
|
||||
|
||||
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) {
|
||||
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(`该手机号码:${customer.phone}已经被使用`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.page.locator('.person_content').waitFor();
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置顾客为无效客
|
||||
* @param {Customer} customer 顾客
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
setInvalidCustomer = async (customer) => {
|
||||
await this.page.goto(process.env.BASE_URL || '', { waitUntil: 'load' });
|
||||
const moduleLocator = this.$module;
|
||||
const activeLocator = moduleLocator.locator('.active_arrow');
|
||||
await expect(async () => {
|
||||
if (!(await activeLocator.isVisible())) {
|
||||
await moduleLocator.click({ clickCount: 1 });
|
||||
}
|
||||
await expect(activeLocator).toBeVisible({ timeout: 2_000 });
|
||||
}).toPass({ timeout: 30_000 });
|
||||
|
||||
// 根据手机号进行搜索,进入顾客详情页面
|
||||
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 {Array<Customer>} customerArray
|
||||
*/
|
||||
createMoreCustomer = async (customerArray) => {
|
||||
for (const customer of customerArray) {
|
||||
await this.createCustomer(customer);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量设置无效客
|
||||
* @param {Array<Customer>} customerArray
|
||||
*/
|
||||
setMoreInvalidCustomer = async (customerArray) => {
|
||||
for (const customer of customerArray) {
|
||||
await this.setInvalidCustomer(customer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { CustomerPage };
|
||||
19
tests/hlk/pom/h5LoginPage.js
Normal file
19
tests/hlk/pom/h5LoginPage.js
Normal file
@ -0,0 +1,19 @@
|
||||
class H5LoginPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.$account = this.page.locator('.input', { hasText: '请输入手机号码' }).locator('input');
|
||||
this.$SMSCode = this.page.locator('.input', { hasText: '请输入验证码' }).locator('input');
|
||||
this.$loginBtn = this.page.locator('uni-button').filter({ hasText: /^登录$/ });
|
||||
}
|
||||
|
||||
login = async (account) => {
|
||||
await this.page.locator('.login_btn').click();
|
||||
await this.$account.click();
|
||||
await this.$account.fill(account);
|
||||
await this.$SMSCode.fill(process.env.SMSCODE);
|
||||
await this.page.getByText('同意隐私协议').click();
|
||||
await this.$loginBtn.click();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { H5LoginPage };
|
||||
58
tests/hlk/pom/homeNavigationPage.js
Normal file
58
tests/hlk/pom/homeNavigationPage.js
Normal file
@ -0,0 +1,58 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
export class HomeNavigation {
|
||||
/**
|
||||
* @param {import("@playwright/test").Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.$moduleList = this.page.locator('.left_box .link_item');
|
||||
this.pages = {
|
||||
cash: '收银',
|
||||
appointment: '预约',
|
||||
member: '顾客',
|
||||
flow: '流水',
|
||||
stock: '库存',
|
||||
management: '目标',
|
||||
marketing: '营销',
|
||||
report: '报表',
|
||||
setting: '设置',
|
||||
other: '其他',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到对应模块
|
||||
* @param {string} pageName 模块名称
|
||||
* - cash-收银
|
||||
* - appointment-预约
|
||||
* - member-顾客
|
||||
* - flow-流水
|
||||
* - stock-库存
|
||||
* - management-目标
|
||||
* - marketing-营销
|
||||
* - report-报表
|
||||
* - setting-设置
|
||||
* - other-其他
|
||||
*/
|
||||
gotoModule = async (pageName) => {
|
||||
const moduleName = this.pages[pageName];
|
||||
|
||||
if (!moduleName) {
|
||||
throw new Error(`Page ${pageName} does not exist`);
|
||||
}
|
||||
// 模块定位器
|
||||
const moduleLocator = this.$moduleList.filter({ hasText: moduleName });
|
||||
// 当前模块选中提示定位器
|
||||
const activeLocator = moduleLocator.locator('.active_arrow');
|
||||
|
||||
await expect(moduleLocator).toBeInViewport();
|
||||
|
||||
await expect(async () => {
|
||||
if (!(await activeLocator.isVisible())) {
|
||||
await moduleLocator.click({ clickCount: 1 });
|
||||
}
|
||||
await expect(activeLocator).toBeInViewport();
|
||||
}).toPass({ timeout: 30_000 });
|
||||
};
|
||||
}
|
||||
0
tests/hlk/pom/touchCustomerPage.js
Normal file
0
tests/hlk/pom/touchCustomerPage.js
Normal file
22
tests/hlk/pom/touchLoginPage.js
Normal file
22
tests/hlk/pom/touchLoginPage.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { expect } = require('@playwright/test');
|
||||
class TouchLoginPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.$account = this.page.getByRole('textbox', { name: '请输入您的手机号码' });
|
||||
this.$password = this.page.getByRole('textbox', { name: '请输入登录密码' });
|
||||
this.$loginBtn = this.page.getByRole('button', { name: /登\s录/ });
|
||||
}
|
||||
|
||||
login = async (account, password) => {
|
||||
await this.$account.fill(account);
|
||||
const phoneStatsIcon = this.page
|
||||
.locator('.ant-row', { has: this.$account })
|
||||
.locator('.pass_svg');
|
||||
await this.$password.fill(password);
|
||||
await this.page.getByLabel('请同意慧来客隐私政策和用户协议').check();
|
||||
await expect(phoneStatsIcon).toBeVisible();
|
||||
await this.$loginBtn.click();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { TouchLoginPage };
|
||||
22
tests/hlk/setup/hlk.setup.js
Normal file
22
tests/hlk/setup/hlk.setup.js
Normal file
@ -0,0 +1,22 @@
|
||||
const { test: setup, expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
const hlkAuthFile = path.join(process.cwd(), '.auth', 'hlk_admin.json');
|
||||
|
||||
setup('hlk总部管理员登录', async ({ page, baseURL }) => {
|
||||
const $account = page.getByRole('textbox', { name: '请输入您的手机号码' });
|
||||
const $password = page.getByRole('textbox', { name: '请输入登录密码' });
|
||||
const $loginBtn = page.getByRole('button', { name: /登\s录/ });
|
||||
|
||||
const account = process.env.ACCOUNT;
|
||||
const password = process.env.PASSWORD;
|
||||
|
||||
await page.goto(baseURL);
|
||||
await $account.fill(account);
|
||||
const phoneStatsIcon = page.locator('.ant-row', { has: $account }).locator('.pass_svg');
|
||||
await $password.fill(password);
|
||||
await page.getByLabel('请同意慧来客隐私政策和用户协议').check();
|
||||
await expect(phoneStatsIcon).toBeVisible();
|
||||
await $loginBtn.click();
|
||||
await expect(page.getByRole('button', { name: /开\s单/ })).toBeVisible();
|
||||
await page.context().storageState({ path: hlkAuthFile });
|
||||
});
|
||||
56
tests/mgj/demo.spec.js
Normal file
56
tests/mgj/demo.spec.js
Normal file
@ -0,0 +1,56 @@
|
||||
const { test, expect } = require('./fixture/common');
|
||||
const { Customer } = require('./pom/customerPage');
|
||||
|
||||
test('demo', async ({ mgjPage, customerPage }) => {
|
||||
await test.step('创建顾客', async () => {
|
||||
await expect(mgjPage.locator('#autoStationTip').getByText('我知道了')).toBeVisible();
|
||||
await mgjPage.locator('#autoStationTip').getByText('我知道了').click();
|
||||
await mgjPage.locator('#tab_main li').filter({ hasText: '顾客' }).click();
|
||||
const customer = new Customer();
|
||||
await customerPage.createCustomer(customer);
|
||||
});
|
||||
|
||||
await test.step('开单结算,购买第一个项目', async () => {
|
||||
await mgjPage.getByText('去开单').click();
|
||||
await mgjPage.locator('span').filter({ hasText: '确认开单' }).click();
|
||||
const firstEnabledElement = mgjPage
|
||||
.locator('#createService .content ul .am-clickable:not(.am-disabled)')
|
||||
.first();
|
||||
await mgjPage.waitForTimeout(1000);
|
||||
await firstEnabledElement.click();
|
||||
|
||||
await expect(
|
||||
mgjPage.locator('#cashierTotalPanel span').filter({ hasText: /结\s算/ })
|
||||
).toBeVisible();
|
||||
|
||||
await mgjPage.waitForTimeout(1000);
|
||||
|
||||
// 选择第一个项目
|
||||
await mgjPage.locator('.cashierItems .am-clickable').first().click();
|
||||
await expect(mgjPage.locator('.cashierBox tbody tr.am-clickable').first()).toBeVisible();
|
||||
await mgjPage
|
||||
.locator('#cashierTotalPanel span')
|
||||
.filter({ hasText: /结\s算/ })
|
||||
.click();
|
||||
|
||||
await expect(async () => {
|
||||
const $signature = mgjPage.locator('#page_pay').getByText('结算签字');
|
||||
await $signature.click();
|
||||
await expect($signature).not.toHaveClass(/checked/, { timeout: 2000 });
|
||||
}).toPass();
|
||||
|
||||
await expect(async () => {
|
||||
const $cashBtn = mgjPage.locator('#page_pay .pay_cash');
|
||||
await $cashBtn.click();
|
||||
await expect(mgjPage.locator('#page_pay .pay_cash.selected')).toBeVisible();
|
||||
await mgjPage
|
||||
.locator('#page_pay span')
|
||||
.filter({ hasText: /结\s算/ })
|
||||
.click();
|
||||
await expect(mgjPage.getByText('顾客满意度点评')).toBeVisible({ timeout: 2000 });
|
||||
}).toPass();
|
||||
|
||||
await mgjPage.getByText('不想评价').click();
|
||||
await expect(mgjPage.locator('#page_footBathPay').getByText('立即返回')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
25
tests/mgj/fixture/base.js
Normal file
25
tests/mgj/fixture/base.js
Normal file
@ -0,0 +1,25 @@
|
||||
const { test: base, expect } = require('@playwright/test');
|
||||
const authFile = '.auth/mgj_admin.json';
|
||||
const test = base.extend({
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
mgjPage: async ({ browser, baseURL }, use) => {
|
||||
const context = await browser.newContext({
|
||||
storageState: authFile,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(baseURL);
|
||||
console.log(baseURL);
|
||||
|
||||
await use(page);
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
test,
|
||||
expect,
|
||||
};
|
||||
6
tests/mgj/fixture/common.js
Normal file
6
tests/mgj/fixture/common.js
Normal file
@ -0,0 +1,6 @@
|
||||
const { mergeTests } = require('@playwright/test');
|
||||
const { test: customerTest } = require('./customerFixture');
|
||||
|
||||
export const test = mergeTests(customerTest);
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
12
tests/mgj/fixture/customerFixture.js
Normal file
12
tests/mgj/fixture/customerFixture.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { test as base } from './base';
|
||||
import { CustomerPage } from '../pom/customerPage';
|
||||
|
||||
export const test = base.extend({
|
||||
/**
|
||||
* @type {CustomerPage}
|
||||
*/
|
||||
customerPage: async ({ mgjPage }, use) => {
|
||||
const customerPage = new CustomerPage(mgjPage);
|
||||
await use(customerPage);
|
||||
},
|
||||
});
|
||||
14
tests/mgj/fixture/h5Fixture.js
Normal file
14
tests/mgj/fixture/h5Fixture.js
Normal file
@ -0,0 +1,14 @@
|
||||
const { test: base, expect } = require('./base');
|
||||
const { H5LoginPage } = require('../pom/hlk/h5LoginPage');
|
||||
|
||||
const test = base.extend({
|
||||
/**
|
||||
* @type {H5LoginPage}
|
||||
*/
|
||||
h5LoginPage: async ({ h5Page }, use) => {
|
||||
const h5LoginPage = new H5LoginPage(h5Page);
|
||||
await use(h5LoginPage);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = { test, expect };
|
||||
54
tests/mgj/pom/customerPage.js
Normal file
54
tests/mgj/pom/customerPage.js
Normal file
@ -0,0 +1,54 @@
|
||||
const { faker } = require('@faker-js/faker/locale/zh_CN');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
export class Customer {
|
||||
constructor({
|
||||
name = faker.person.fullName(),
|
||||
phone = faker.helpers.fromRegExp(/1[3-9][0-9]{6}/),
|
||||
} = {}) {
|
||||
this.name = name;
|
||||
this.phone = phone;
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomerPage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建顾客
|
||||
* @param {Customer} customer
|
||||
*/
|
||||
createCustomer = async (customer) => {
|
||||
console.log(customer.name);
|
||||
console.log(customer.phone);
|
||||
|
||||
await expect(this.page.locator('#page_member').getByText('新增顾客档案')).toBeVisible();
|
||||
await this.page.locator('#page_member').getByText('新增顾客档案').click();
|
||||
|
||||
await this.page.getByPlaceholder('请输入会员姓名').fill(customer.name);
|
||||
await this.page.getByPlaceholder('请输入手机号码').click();
|
||||
await expect(this.page.getByText('请输入数字')).toBeVisible();
|
||||
await this.page.getByPlaceholder('在此输入').click();
|
||||
await this.page.keyboard.type(customer.phone, { delay: 100 });
|
||||
// await this.page.getByPlaceholder('在此输入').type(customer.phone, { delay: 100 });
|
||||
await this.page.locator('#maskBoard').getByText('确认').click();
|
||||
await expect(this.page.getByPlaceholder('请输入手机号码')).toHaveValue(customer.phone, {
|
||||
timeout: 2000,
|
||||
});
|
||||
await this.page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^员工带客$/ })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await this.page.getByText('创建并选择').click();
|
||||
await expect(this.page.getByText('用户资料创建成功!')).toBeVisible();
|
||||
await this.page.getByText('以后再说').click();
|
||||
await expect(this.page.getByText('以后再说')).not.toBeVisible();
|
||||
};
|
||||
}
|
||||
19
tests/mgj/pom/h5LoginPage.js
Normal file
19
tests/mgj/pom/h5LoginPage.js
Normal file
@ -0,0 +1,19 @@
|
||||
class H5LoginPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.$account = this.page.locator('.input', { hasText: '请输入手机号码' }).locator('input');
|
||||
this.$SMSCode = this.page.locator('.input', { hasText: '请输入验证码' }).locator('input');
|
||||
this.$loginBtn = this.page.locator('uni-button').filter({ hasText: /^登录$/ });
|
||||
}
|
||||
|
||||
login = async (account) => {
|
||||
await this.page.locator('.login_btn').click();
|
||||
await this.$account.click();
|
||||
await this.$account.fill(account);
|
||||
await this.$SMSCode.fill(process.env.SMSCODE);
|
||||
await this.page.getByText('同意隐私协议').click();
|
||||
await this.$loginBtn.click();
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { H5LoginPage };
|
||||
17
tests/mgj/setup/mgj.setup.js
Normal file
17
tests/mgj/setup/mgj.setup.js
Normal file
@ -0,0 +1,17 @@
|
||||
const { test: setup, expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
const mgjAuthFile = path.join(process.cwd(), '.auth', 'mgj_admin.json');
|
||||
|
||||
setup('mgj管理员登录', async ({ page, baseURL }) => {
|
||||
const account = process.env.MGJ_ACCOUNT;
|
||||
const password = process.env.MGJ_PASSWORD;
|
||||
|
||||
await page.goto(baseURL);
|
||||
await page.getByText('点击此处帐号登录').click();
|
||||
await page.getByPlaceholder('用户名').fill(account);
|
||||
await page.getByPlaceholder('请输入密码').fill(password);
|
||||
await page.getByText('登录', { exact: true }).click();
|
||||
await page.locator('#shopSelect').getByText('一店').click();
|
||||
await expect(page.locator('#autoStationTip').getByText('我知道了')).toBeVisible();
|
||||
await page.context().storageState({ path: mgjAuthFile });
|
||||
});
|
||||
118
tests/zhb/csv-demo.spec.js
Normal file
118
tests/zhb/csv-demo.spec.js
Normal file
@ -0,0 +1,118 @@
|
||||
const path = require('path');
|
||||
const { parse } = require('csv-parse/sync');
|
||||
const fs = require('fs');
|
||||
const { faker } = require('@faker-js/faker');
|
||||
const { test, expect } = require('./fixture/common');
|
||||
const { Customer } = require('./pom/customerPage');
|
||||
|
||||
const records = parse(fs.readFileSync(path.join(__dirname, 'zhb.csv')), {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
});
|
||||
|
||||
test('csv', async ({ zhbPage }, workerInfo) => {
|
||||
console.log(records.length);
|
||||
const $area = zhbPage
|
||||
.locator('.area')
|
||||
.filter({ has: zhbPage.locator('.area-name', { hasText: '二楼' }) });
|
||||
const $$room = $area.locator('.room-list .room');
|
||||
|
||||
// 随机csv文件中的任意一个顾客
|
||||
const record = records[faker.number.int({ min: 0, max: records.length - 1 })];
|
||||
const { name, phone } = record;
|
||||
console.log({ name, phone });
|
||||
const customer = new Customer({ name: name, phone: name });
|
||||
|
||||
// 使用房间名称
|
||||
let useRoomName;
|
||||
await test.step('购买商品', async () => {
|
||||
await zhbPage.locator('#tab_main li').filter({ hasText: '营业' }).click();
|
||||
const $emptyRoom = $$room
|
||||
.filter({ has: zhbPage.getByText('空房') })
|
||||
.nth(workerInfo.workerIndex % 3);
|
||||
useRoomName = await $emptyRoom.locator('.roomName').innerText();
|
||||
expect(useRoomName).not.toBeNull();
|
||||
await $emptyRoom.click();
|
||||
await expect(async () => {
|
||||
if (await zhbPage.locator('.close > .iconfont').first().isVisible()) {
|
||||
await zhbPage.locator('.close > .iconfont').first().click();
|
||||
}
|
||||
await zhbPage.getByRole('button', { name: '选择顾客' }).click({ timeout: 3000 });
|
||||
await expect(zhbPage.locator('#page_searchMember').getByText('创建会员')).toBeVisible();
|
||||
}).toPass();
|
||||
await zhbPage
|
||||
.getByRole('textbox', { name: '输入会员手机号或姓名或卡号搜索' })
|
||||
.fill(customer.phone, { delay: 100 });
|
||||
await zhbPage.locator('#page_searchMember svg').click();
|
||||
|
||||
const $customerTr = zhbPage
|
||||
.locator('.list-warp')
|
||||
.filter({ has: zhbPage.locator('.name', { hasText: customer.name }) });
|
||||
await $customerTr.locator('.list-body').first().click();
|
||||
|
||||
await zhbPage.getByText('项目开单').click();
|
||||
await expect(zhbPage.locator('#page_roomDetail').getByText('服务项目')).toBeVisible();
|
||||
await zhbPage.getByText('选择', { exact: true }).nth(1).click();
|
||||
await expect(zhbPage.locator('#serviceSelector').getByText('项目选择')).toBeVisible();
|
||||
await zhbPage
|
||||
.locator('.goods-content-item')
|
||||
.nth(faker.number.int({ min: 0, max: 14 }))
|
||||
.click();
|
||||
await zhbPage.locator('#serviceSelector').getByText('确认').click();
|
||||
await zhbPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^明星足浴$/ })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await zhbPage.getByRole('button', { name: '完成开单' }).click();
|
||||
await expect(zhbPage.getByRole('button', { name: '结账' })).toBeVisible({ timeout: 30_000 });
|
||||
await zhbPage.getByRole('button', { name: '结账' }).click();
|
||||
await zhbPage.locator('#page_footBathPay').getByText('结算签字').click();
|
||||
|
||||
await expect(async () => {
|
||||
await zhbPage.locator('#page_footBathPay li').filter({ hasText: '现金' }).click();
|
||||
await expect(zhbPage.locator('#page_footBathPay li').filter({ hasText: '现金' })).toHaveClass(
|
||||
/selected/
|
||||
);
|
||||
await zhbPage
|
||||
.locator('#page_footBathPay span')
|
||||
.filter({ hasText: /结\s算/ })
|
||||
.click();
|
||||
await expect(zhbPage.getByText('顾客满意度点评')).toBeVisible({ timeout: 2000 });
|
||||
}).toPass();
|
||||
|
||||
await zhbPage.getByText('不想评价').click();
|
||||
await zhbPage.locator('#page_footBathPay').getByText('立即返回').click();
|
||||
});
|
||||
|
||||
await test.step('起钟下钟,清理房间', async () => {
|
||||
const $cleanRoom = $$room.filter({
|
||||
has: zhbPage.locator('.roomName', { hasText: new RegExp(`^${useRoomName}$`) }),
|
||||
});
|
||||
await expect($cleanRoom).toContainText('已结清');
|
||||
await $cleanRoom.click();
|
||||
await zhbPage.getByText('技师操作').click();
|
||||
await zhbPage.getByText('起钟', { exact: true }).click();
|
||||
await zhbPage.getByText('起钟成功!').click();
|
||||
await zhbPage.getByText('技师操作').click();
|
||||
await zhbPage.getByText('下钟', { exact: true }).click();
|
||||
await zhbPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^确认返回$/ })
|
||||
.locator('div')
|
||||
.first()
|
||||
.click();
|
||||
await zhbPage.getByRole('button', { name: '不需要' }).click();
|
||||
|
||||
await expect($cleanRoom).toContainText('打扫');
|
||||
await $cleanRoom.click();
|
||||
await zhbPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^确定取消$/ })
|
||||
.locator('div')
|
||||
.first()
|
||||
.click();
|
||||
await expect($cleanRoom).toContainText('空房');
|
||||
});
|
||||
});
|
||||
67
tests/zhb/fixture/base.js
Normal file
67
tests/zhb/fixture/base.js
Normal file
@ -0,0 +1,67 @@
|
||||
const { test: base, expect, devices } = require('@playwright/test');
|
||||
const zhbAuthFile = '.auth/zhb.json';
|
||||
const zhbAdminAuthFile = '.auth/zhb_admin.json';
|
||||
const test = base.extend({
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
zhbPage: async ({ browser, baseURL }, use) => {
|
||||
const context = await browser.newContext({
|
||||
storageState: zhbAuthFile,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(baseURL);
|
||||
console.log(baseURL);
|
||||
|
||||
await use(page);
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
},
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
zhbAdminPage: async ({ browser }, use) => {
|
||||
const baseURL = process.env.ZHB_ADMIN_URL;
|
||||
const account = process.env.ZHB_ACCOUNT;
|
||||
const password = process.env.ZHB_PASSWORD;
|
||||
const context = await browser.newContext({
|
||||
storageState: zhbAdminAuthFile,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(baseURL);
|
||||
console.log(baseURL);
|
||||
|
||||
await page.getByPlaceholder('账户').fill(account);
|
||||
await page.getByPlaceholder('密码').fill(password);
|
||||
await page.getByRole('button', { name: '登 录' }).click();
|
||||
await expect(page.locator('a').filter({ hasText: /^收银$/ })).toBeVisible();
|
||||
|
||||
await use(page);
|
||||
|
||||
await page.close();
|
||||
await context.close();
|
||||
},
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
h5Page: async ({ browser }, use) => {
|
||||
const baseURL = process.env.ZHB_H5_URL;
|
||||
const iPhone = devices['iPhone 14'];
|
||||
const context = await browser.newContext({
|
||||
...iPhone,
|
||||
storageState: undefined,
|
||||
hasTouch: true,
|
||||
isMobile: true, // 设置为移动设备
|
||||
viewport: iPhone.viewport, // 使用 iPhone 的视口配置
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto(baseURL);
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
test,
|
||||
expect,
|
||||
};
|
||||
7
tests/zhb/fixture/common.js
Normal file
7
tests/zhb/fixture/common.js
Normal file
@ -0,0 +1,7 @@
|
||||
const { mergeTests } = require('@playwright/test');
|
||||
const { test: baseTest } = require('./base');
|
||||
const { test: customerTest } = require('./customerFixture');
|
||||
|
||||
export const test = mergeTests(baseTest, customerTest);
|
||||
|
||||
export { expect } from '@playwright/test';
|
||||
12
tests/zhb/fixture/customerFixture.js
Normal file
12
tests/zhb/fixture/customerFixture.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { test as base } from './base';
|
||||
import { CustomerPage } from '../pom/customerPage';
|
||||
|
||||
export const test = base.extend({
|
||||
/**
|
||||
* @type {CustomerPage}
|
||||
*/
|
||||
customerPage: async ({ zhbPage }, use) => {
|
||||
const customerPage = new CustomerPage(zhbPage);
|
||||
await use(customerPage);
|
||||
},
|
||||
});
|
||||
51
tests/zhb/pom/customerPage.js
Normal file
51
tests/zhb/pom/customerPage.js
Normal file
@ -0,0 +1,51 @@
|
||||
const { faker } = require('@faker-js/faker/locale/zh_CN');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
export class Customer {
|
||||
constructor({
|
||||
name = faker.person.fullName(),
|
||||
phone = faker.helpers.fromRegExp(/123[0-5]{8}/),
|
||||
} = {}) {
|
||||
this.name = name;
|
||||
this.phone = phone;
|
||||
}
|
||||
}
|
||||
|
||||
export class CustomerPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建顾客
|
||||
* @param {Customer} customer
|
||||
*/
|
||||
createCustomer = async (customer) => {
|
||||
console.log(customer.name);
|
||||
console.log(customer.phone);
|
||||
|
||||
await expect(this.page.locator('#page_member').getByText('新增顾客档案')).toBeVisible();
|
||||
await this.page.locator('#page_member').getByText('新增顾客档案').click();
|
||||
|
||||
await this.page.getByPlaceholder('请输入会员姓名').fill(customer.name);
|
||||
await this.page.getByPlaceholder('请输入手机号码').click();
|
||||
await expect(this.page.getByText('请输入数字')).toBeVisible();
|
||||
await this.page.getByPlaceholder('在此输入').click();
|
||||
await this.page.keyboard.type(customer.phone, { delay: 50 });
|
||||
// await this.page.getByPlaceholder('在此输入').type(customer.phone, { delay: 100 });
|
||||
await this.page.locator('#maskBoard').getByText('确认').click();
|
||||
await expect(this.page.getByPlaceholder('请输入手机号码')).toHaveValue(customer.phone, {
|
||||
timeout: 2000,
|
||||
});
|
||||
await this.page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^员工带客$/ })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await this.page.getByText('创建', { exact: true }).click();
|
||||
await expect(this.page.getByText('用户资料创建成功!')).toBeVisible();
|
||||
await this.page.getByText('以后再说').click();
|
||||
await expect(this.page.getByText('以后再说')).not.toBeVisible();
|
||||
};
|
||||
}
|
||||
25
tests/zhb/pom/h5LoginPage.js
Normal file
25
tests/zhb/pom/h5LoginPage.js
Normal file
@ -0,0 +1,25 @@
|
||||
const { processAndRecognizeCaptcha } = require('../../utils/helper');
|
||||
|
||||
class H5LoginPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.$account = this.page.getByPlaceholder('此处请输入手机号');
|
||||
this.$GraphCode = this.page.getByRole('textbox', { name: '请输入图形验证码' });
|
||||
this.$SmsCode = this.page.getByRole('textbox', { name: '验证码', exact: true });
|
||||
this.$loginBtn = this.page.locator('uni-button').filter({ hasText: /^登录$/ });
|
||||
}
|
||||
|
||||
sendSmsCode = async (account) => {
|
||||
const imagePath = '.images/captcha.png';
|
||||
const outputImagePath = '.images/output-captcha.png';
|
||||
|
||||
await this.$account.fill(account);
|
||||
const $graph = this.page.getByRole('img', { name: '图形验证码' });
|
||||
await $graph.waitFor();
|
||||
await $graph.screenshot({ path: imagePath });
|
||||
|
||||
return await processAndRecognizeCaptcha(imagePath, outputImagePath);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { H5LoginPage };
|
||||
20
tests/zhb/setup/zhb.setup.js
Normal file
20
tests/zhb/setup/zhb.setup.js
Normal file
@ -0,0 +1,20 @@
|
||||
const { test: setup, expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
const zhbAuthFile = path.join(process.cwd(), '.auth', 'zhb.json');
|
||||
|
||||
setup('zhb总部管理员登录', async ({ page, baseURL }) => {
|
||||
const account = process.env.ZHB_ACCOUNT;
|
||||
const password = process.env.ZHB_PASSWORD;
|
||||
|
||||
await page.goto(baseURL);
|
||||
await page.getByText('账号登录').click();
|
||||
await page.getByPlaceholder('用户名').fill(account);
|
||||
await page.getByPlaceholder('密码', { exact: true }).fill(password);
|
||||
await page.getByText('登录', { exact: true }).click();
|
||||
await expect(page.getByText('演示一店')).toBeVisible();
|
||||
await expect(page.getByText('演示二店')).toBeVisible();
|
||||
|
||||
await page.getByText('演示一店').click();
|
||||
await expect(page.locator('#tab_main li').filter({ hasText: '营业' })).toBeVisible();
|
||||
await page.context().storageState({ path: zhbAuthFile });
|
||||
});
|
||||
201
tests/zhb/zhb.csv
Normal file
201
tests/zhb/zhb.csv
Normal file
@ -0,0 +1,201 @@
|
||||
name,phone
|
||||
郸志国,12334502225
|
||||
纵佳琪,12352301430
|
||||
马熙瑶,12350424001
|
||||
应国平,12314501244
|
||||
委强,12322301255
|
||||
吕苡沫,12314440133
|
||||
柴沐辰,12311131155
|
||||
史国香,12333211154
|
||||
易天娇,12351430503
|
||||
靖敬彪,12321535101
|
||||
贡秀珍,12352123355
|
||||
悉艺涵,12311403141
|
||||
丁国芳,12343515153
|
||||
喻芳,12311322132
|
||||
析晨阳,12344201554
|
||||
酒建国,12332133555
|
||||
窦智杰,12304133243
|
||||
玉子豪,12314500535
|
||||
渠建军,12314142045
|
||||
甲婷婷,12340453445
|
||||
巫娟,12320023214
|
||||
扈斌,12324211053
|
||||
位杰,12311524133
|
||||
斛沐阳,12311151435
|
||||
驹志国,12305534111
|
||||
盘美方,12322214351
|
||||
柯婷,12314322025
|
||||
马涛,12340204331
|
||||
东政君,12332111030
|
||||
梁玉兰,12355420144
|
||||
安丽,12302350005
|
||||
赫俊杰,12341541431
|
||||
尉迟海燕,12313530222
|
||||
礼国珍,12321141225
|
||||
行梓豪,12343224431
|
||||
拉国荣,12351411412
|
||||
练国栋,12312010324
|
||||
戎沐宸,12343432310
|
||||
翟宇,12344114011
|
||||
桂梓晨,12340344054
|
||||
斋志明,12305321102
|
||||
及万佳,12335454335
|
||||
前雪,12345534331
|
||||
伦英,12321322045
|
||||
柏立伟,12345251522
|
||||
招欣怡,12324401040
|
||||
首茗泽,12323330351
|
||||
计乙萍,12303514002
|
||||
线婷方,12324501502
|
||||
九秀珍,12344040353
|
||||
茂蒙,12312210221
|
||||
红婷方,12340215445
|
||||
扈倩,12354532031
|
||||
原立伟,12342524245
|
||||
祖丽芳,12322124425
|
||||
蓬雨欣,12340321430
|
||||
定瑜,12350252355
|
||||
望丽萍,12311313430
|
||||
铁家豪,12352502012
|
||||
闳雅鑫,12313242150
|
||||
单于燕,12314042132
|
||||
杜奕泽,12345103503
|
||||
尉丽芳,12323404415
|
||||
徭国平,12321014220
|
||||
羽志国,12311511234
|
||||
弥安琪,12322541510
|
||||
阮梓玥,12302200542
|
||||
呼波,12351535225
|
||||
玉呈轩,12304451524
|
||||
针熙瑶,12354523544
|
||||
帖国平,12305535553
|
||||
班天娇,12330411042
|
||||
藏秀珍,12304203153
|
||||
计国琴,12351531450
|
||||
鄢国珍,12343500300
|
||||
屈国兰,12313202405
|
||||
齐子欣,12355341152
|
||||
曹安琪,12355140413
|
||||
甫建华,12354544421
|
||||
苗丽芬,12311142041
|
||||
完倩,12324441204
|
||||
綦雯静,12304221302
|
||||
蔺志明,12322530542
|
||||
磨晨阳,12315334001
|
||||
友秀珍,12312034423
|
||||
历燕,12330435540
|
||||
飞文昊,12315150424
|
||||
萧鹏,12333512443
|
||||
用中海,12334505210
|
||||
竭智杰,12312530202
|
||||
次国琴,12331300513
|
||||
乙一诺,12304041145
|
||||
析志明,12340411012
|
||||
曲红,12320421131
|
||||
闫馨羽,12332235531
|
||||
卷语桐,12305350101
|
||||
旷丹,12351401021
|
||||
植玉梅,12340034300
|
||||
祖国栋,12310142153
|
||||
乙艺涵,12301532402
|
||||
海芳,12313345003
|
||||
枝敬彪,12324501125
|
||||
雍国秀,12340243412
|
||||
章佳紫林,12320333352
|
||||
充丽萍,12322423001
|
||||
百雅鑫,12333040513
|
||||
鹿治涛,12324533401
|
||||
慎文韬,12350423141
|
||||
谯梓涵,12301052044
|
||||
针振东,12353344501
|
||||
绍奕辰,12350005540
|
||||
仉文昊,12332320502
|
||||
元梓玥,12335424410
|
||||
祝浩宇,12311335301
|
||||
来斌,12300325534
|
||||
刁志明,12320244541
|
||||
首兰英,12311150011
|
||||
练安琪,12342301055
|
||||
唐雯静,12312505520
|
||||
郝天娇,12312520301
|
||||
庄丽芳,12353444410
|
||||
源依诺,12342341501
|
||||
诸丽芬,12312415203
|
||||
朱慧,12344511420
|
||||
绍国辉,12341315304
|
||||
肥志明,12304445132
|
||||
包文韬,12330305102
|
||||
钱倩,12350342140
|
||||
樊丽芳,12332020302
|
||||
钱雨欣,12304114004
|
||||
鱼静,12331411232
|
||||
速宇泽,12331502245
|
||||
叔沐宸,12335305514
|
||||
进国英,12314454234
|
||||
费慧,12304155020
|
||||
漆治文,12352421501
|
||||
示雪,12353154301
|
||||
劳颖,12343235112
|
||||
谈馥君,12343113130
|
||||
兆呈轩,12320135205
|
||||
萧文韬,12304320034
|
||||
漫军,12321552123
|
||||
施国珍,12330141235
|
||||
纳静怡,12341035053
|
||||
贰阳,12305451142
|
||||
呼义轩,12315402403
|
||||
植语桐,12335354425
|
||||
路国荣,12304330054
|
||||
支梓豪,12333434504
|
||||
蔚宇航,12321411124
|
||||
第宇航,12342053534
|
||||
校海燕,12301142130
|
||||
纳喇诗雨,12323221524
|
||||
浑呈轩,12351243530
|
||||
弓梓馨,12353030231
|
||||
常玉梅,12352425540
|
||||
冀丽萍,12343501540
|
||||
北民,12321312355
|
||||
栋勇,12321145441
|
||||
原桂兰,12352325455
|
||||
陈榕融,12315235050
|
||||
况伟,12353031413
|
||||
丁浩辰,12345310354
|
||||
郭斌,12352334430
|
||||
巴蒙,12344251134
|
||||
可艳,12310255512
|
||||
玄浩辰,12331503043
|
||||
沙呈轩,12302013431
|
||||
荤成,12344214445
|
||||
林熙瑶,12305423343
|
||||
葛梓睿,12314114055
|
||||
湛斌,12345150514
|
||||
由静,12300130254
|
||||
侯振东,12324015213
|
||||
贺单,12301145101
|
||||
所宇轩,12331420152
|
||||
颜颖,12300005420
|
||||
咎玉梅,12352454144
|
||||
占俊杰,12324402343
|
||||
觉罗秀珍,12311240034
|
||||
范姜丽,12330542141
|
||||
弘梓浩,12343343451
|
||||
通雨欣,12331513024
|
||||
丘梓诚,12344112043
|
||||
贵梓晨,12311414030
|
||||
狄悦,12332305454
|
||||
穆凤英,12335205455
|
||||
金智杰,12301542505
|
||||
蒙癸霖,12305252022
|
||||
诺兰英,12303005354
|
||||
帛娟,12304243114
|
||||
在国英,12355121223
|
||||
严芳,12355412233
|
||||
狄治文,12320503352
|
||||
律雨欣,12345523042
|
||||
偶敬彪,12332433431
|
||||
求佳琪,12300144202
|
||||
唐中海,12351305521
|
||||
第治涛,12300442432
|
||||
壬家明,12335343552
|
||||
|
Reference in New Issue
Block a user