init
Some checks failed
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
rsgltzyd 2024-10-15 22:17:14 +08:00 committed by LingandRX
commit db0603dcfd
35 changed files with 2092 additions and 0 deletions

16
.env Normal file
View 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
View 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
View 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
View 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
View 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
View 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,
// },
});

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);
}

60
tests/hlk/demo.spec.js Normal file
View 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();
});
});

View 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,
};

View 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';

View 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 };

View 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
View 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;
}
}

View 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 };

View 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 };

View 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 });
};
}

View File

View 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 };

View 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
View 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
View 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,
};

View 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';

View 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);
},
});

View 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 };

View 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();
};
}

View 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 };

View 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
View 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
View 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,
};

View 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';

View 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);
},
});

View 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();
};
}

View 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 };

View 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
View 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
1 name phone
2 郸志国 12334502225
3 纵佳琪 12352301430
4 马熙瑶 12350424001
5 应国平 12314501244
6 委强 12322301255
7 吕苡沫 12314440133
8 柴沐辰 12311131155
9 史国香 12333211154
10 易天娇 12351430503
11 靖敬彪 12321535101
12 贡秀珍 12352123355
13 悉艺涵 12311403141
14 丁国芳 12343515153
15 喻芳 12311322132
16 析晨阳 12344201554
17 酒建国 12332133555
18 窦智杰 12304133243
19 玉子豪 12314500535
20 渠建军 12314142045
21 甲婷婷 12340453445
22 巫娟 12320023214
23 扈斌 12324211053
24 位杰 12311524133
25 斛沐阳 12311151435
26 驹志国 12305534111
27 盘美方 12322214351
28 柯婷 12314322025
29 马涛 12340204331
30 东政君 12332111030
31 梁玉兰 12355420144
32 安丽 12302350005
33 赫俊杰 12341541431
34 尉迟海燕 12313530222
35 礼国珍 12321141225
36 行梓豪 12343224431
37 拉国荣 12351411412
38 练国栋 12312010324
39 戎沐宸 12343432310
40 翟宇 12344114011
41 桂梓晨 12340344054
42 斋志明 12305321102
43 及万佳 12335454335
44 前雪 12345534331
45 伦英 12321322045
46 柏立伟 12345251522
47 招欣怡 12324401040
48 首茗泽 12323330351
49 计乙萍 12303514002
50 线婷方 12324501502
51 九秀珍 12344040353
52 茂蒙 12312210221
53 红婷方 12340215445
54 扈倩 12354532031
55 原立伟 12342524245
56 祖丽芳 12322124425
57 蓬雨欣 12340321430
58 定瑜 12350252355
59 望丽萍 12311313430
60 铁家豪 12352502012
61 闳雅鑫 12313242150
62 单于燕 12314042132
63 杜奕泽 12345103503
64 尉丽芳 12323404415
65 徭国平 12321014220
66 羽志国 12311511234
67 弥安琪 12322541510
68 阮梓玥 12302200542
69 呼波 12351535225
70 玉呈轩 12304451524
71 针熙瑶 12354523544
72 帖国平 12305535553
73 班天娇 12330411042
74 藏秀珍 12304203153
75 计国琴 12351531450
76 鄢国珍 12343500300
77 屈国兰 12313202405
78 齐子欣 12355341152
79 曹安琪 12355140413
80 甫建华 12354544421
81 苗丽芬 12311142041
82 完倩 12324441204
83 綦雯静 12304221302
84 蔺志明 12322530542
85 磨晨阳 12315334001
86 友秀珍 12312034423
87 历燕 12330435540
88 飞文昊 12315150424
89 萧鹏 12333512443
90 用中海 12334505210
91 竭智杰 12312530202
92 次国琴 12331300513
93 乙一诺 12304041145
94 析志明 12340411012
95 曲红 12320421131
96 闫馨羽 12332235531
97 卷语桐 12305350101
98 旷丹 12351401021
99 植玉梅 12340034300
100 祖国栋 12310142153
101 乙艺涵 12301532402
102 海芳 12313345003
103 枝敬彪 12324501125
104 雍国秀 12340243412
105 章佳紫林 12320333352
106 充丽萍 12322423001
107 百雅鑫 12333040513
108 鹿治涛 12324533401
109 慎文韬 12350423141
110 谯梓涵 12301052044
111 针振东 12353344501
112 绍奕辰 12350005540
113 仉文昊 12332320502
114 元梓玥 12335424410
115 祝浩宇 12311335301
116 来斌 12300325534
117 刁志明 12320244541
118 首兰英 12311150011
119 练安琪 12342301055
120 唐雯静 12312505520
121 郝天娇 12312520301
122 庄丽芳 12353444410
123 源依诺 12342341501
124 诸丽芬 12312415203
125 朱慧 12344511420
126 绍国辉 12341315304
127 肥志明 12304445132
128 包文韬 12330305102
129 钱倩 12350342140
130 樊丽芳 12332020302
131 钱雨欣 12304114004
132 鱼静 12331411232
133 速宇泽 12331502245
134 叔沐宸 12335305514
135 进国英 12314454234
136 费慧 12304155020
137 漆治文 12352421501
138 示雪 12353154301
139 劳颖 12343235112
140 谈馥君 12343113130
141 兆呈轩 12320135205
142 萧文韬 12304320034
143 漫军 12321552123
144 施国珍 12330141235
145 纳静怡 12341035053
146 贰阳 12305451142
147 呼义轩 12315402403
148 植语桐 12335354425
149 路国荣 12304330054
150 支梓豪 12333434504
151 蔚宇航 12321411124
152 第宇航 12342053534
153 校海燕 12301142130
154 纳喇诗雨 12323221524
155 浑呈轩 12351243530
156 弓梓馨 12353030231
157 常玉梅 12352425540
158 冀丽萍 12343501540
159 北民 12321312355
160 栋勇 12321145441
161 原桂兰 12352325455
162 陈榕融 12315235050
163 况伟 12353031413
164 丁浩辰 12345310354
165 郭斌 12352334430
166 巴蒙 12344251134
167 可艳 12310255512
168 玄浩辰 12331503043
169 沙呈轩 12302013431
170 荤成 12344214445
171 林熙瑶 12305423343
172 葛梓睿 12314114055
173 湛斌 12345150514
174 由静 12300130254
175 侯振东 12324015213
176 贺单 12301145101
177 所宇轩 12331420152
178 颜颖 12300005420
179 咎玉梅 12352454144
180 占俊杰 12324402343
181 觉罗秀珍 12311240034
182 范姜丽 12330542141
183 弘梓浩 12343343451
184 通雨欣 12331513024
185 丘梓诚 12344112043
186 贵梓晨 12311414030
187 狄悦 12332305454
188 穆凤英 12335205455
189 金智杰 12301542505
190 蒙癸霖 12305252022
191 诺兰英 12303005354
192 帛娟 12304243114
193 在国英 12355121223
194 严芳 12355412233
195 狄治文 12320503352
196 律雨欣 12345523042
197 偶敬彪 12332433431
198 求佳琪 12300144202
199 唐中海 12351305521
200 第治涛 12300442432
201 壬家明 12335343552