From 6517e4192c122bce4156ef11f72b53ee415c384d Mon Sep 17 00:00:00 2001 From: LingandRX Date: Sun, 22 Dec 2024 19:18:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E6=85=A7?= =?UTF-8?q?=E6=9D=A5=E5=AE=A2=E8=87=AA=E5=8A=A8=E5=8C=96=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=20-=20=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E5=92=8C=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E5=8F=98=E9=87=8F=E8=AE=BE=E7=BD=AE=20-=20=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E7=9B=AE=E5=BD=95=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=92=8C=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83=20-=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=9F=BA=E7=A1=80=E6=B5=8B=E8=AF=95=20fixtur?= =?UTF-8?q?e=20=E5=92=8C=E9=A1=B5=E9=9D=A2=E5=AF=B9=E8=B1=A1=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=20-=20=E6=B7=BB=E5=8A=A0=E7=A4=BA=E4=BE=8B=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=94=A8=E4=BE=8B=E5=92=8C=E6=95=B0=E6=8D=AE=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=20-=20=E9=85=8D=E7=BD=AE=20playwright=20?= =?UTF-8?q?=E5=92=8C=20gitignore=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 22 + .env.staging | 21 + .env.test | 7 + .gitignore | 8 + .prettierrc | 13 + Readme.md | 111 + package.json | 40 + playwright.config.js | 136 + tests-examples/demo-faker.js | 14 + tests-examples/demo-todo-app.spec.js | 449 ++ tests/fixtures/appointmentFixture.ts | 15 + tests/fixtures/baseFixture.ts | 81 + tests/fixtures/boss_common.ts | 28 + tests/fixtures/cashierFixture.ts | 15 + tests/fixtures/componentsFixture.ts | 13 + tests/fixtures/customerFixture.ts | 148 + tests/fixtures/goalFixture.ts | 13 + tests/fixtures/inventoryFixture.ts | 21 + tests/fixtures/marketingFixture.ts | 18 + tests/fixtures/merchantFixture.ts | 52 + tests/fixtures/reportFixture.ts | 66 + tests/fixtures/staff.js | 214 + tests/fixtures/staffFixture.ts | 46 + tests/fixtures/staff_common.ts | 11 + tests/fixtures/tableFixture.ts | 9 + tests/fixtures/userconfig.js | 1020 +++++ tests/fixtures/wasteBookFixture.ts | 14 + .../流水-营业记录-单据明细-补签-1.png | Bin 0 -> 770 bytes tests/imgs/upload.jpg | Bin 0 -> 6125 bytes tests/imgs/商品M.jpg | Bin 0 -> 35888 bytes tests/pages/appointmentPage.ts | 318 ++ tests/pages/cashierRoomPage.ts | 39 + tests/pages/components/index.ts | 3 + tests/pages/components/numberInput.ts | 86 + tests/pages/customer/customerAnalysisPage.ts | 36 + tests/pages/customer/customerDetailsPage.ts | 31 + tests/pages/customer/customerPage.ts | 379 ++ tests/pages/customer/index.ts | 5 + tests/pages/goalPage.ts | 33 + tests/pages/homeNavigationPage.ts | 63 + .../inventory/InventoryManagementPage.ts | 52 + tests/pages/inventory/index.ts | 4 + .../inventoryTransferManagementPage.ts | 43 + tests/pages/marketing/index.ts | 4 + .../marketing/marketingInviteGuestsPage.ts | 80 + tests/pages/marketing/marketingPage.ts | 83 + tests/pages/report/cardBalanceChangeReport.ts | 121 + .../customerConsumptionAnalysisReport.ts | 103 + tests/pages/report/index.ts | 19 + .../itemSalesConsumptionAccessReport.ts | 129 + tests/pages/report/performanceDetailReport.ts | 85 + .../pages/report/performanceSummaryReport.ts | 143 + tests/pages/report/reportPage.ts | 181 + tests/pages/report/salesCostSummaryReport.ts | 87 + tests/pages/report/spendingSummaryReport.ts | 89 + tests/pages/tablePage.ts | 208 + tests/pages/wastebook/index.ts | 3 + .../wastebook/wasteBookBusinessRecordPage.ts | 71 + tests/setup/boss_auth.setup.ts | 70 + tests/setup/staff_auth.setup.ts | 31 + tests/touch/boss_appointment.spec.ts | 638 +++ tests/touch/boss_cashier.spec.ts | 1425 +++++++ tests/touch/boss_customer.spec.ts | 3600 ++++++++++++++++ tests/touch/boss_goal.spec.ts | 1501 +++++++ tests/touch/boss_inventory.spec.ts | 3778 +++++++++++++++++ tests/touch/boss_marketing.spec.ts | 1798 ++++++++ tests/touch/boss_report.spec.ts | 1657 ++++++++ tests/touch/boss_wastebook.spec.ts | 1244 ++++++ tests/touch/staff_goal.spec.ts | 276 ++ tests/touch/staff_inventory.spec.ts | 236 + tests/utils/customer.ts | 40 + tests/utils/indexedDBUtils.ts | 132 + tests/utils/utils.js | 201 + tsconfig.json | 19 + 74 files changed, 21749 insertions(+) create mode 100644 .env create mode 100644 .env.staging create mode 100644 .env.test create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 Readme.md create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 tests-examples/demo-faker.js create mode 100644 tests-examples/demo-todo-app.spec.js create mode 100644 tests/fixtures/appointmentFixture.ts create mode 100644 tests/fixtures/baseFixture.ts create mode 100644 tests/fixtures/boss_common.ts create mode 100644 tests/fixtures/cashierFixture.ts create mode 100644 tests/fixtures/componentsFixture.ts create mode 100644 tests/fixtures/customerFixture.ts create mode 100644 tests/fixtures/goalFixture.ts create mode 100644 tests/fixtures/inventoryFixture.ts create mode 100644 tests/fixtures/marketingFixture.ts create mode 100644 tests/fixtures/merchantFixture.ts create mode 100644 tests/fixtures/reportFixture.ts create mode 100644 tests/fixtures/staff.js create mode 100644 tests/fixtures/staffFixture.ts create mode 100644 tests/fixtures/staff_common.ts create mode 100644 tests/fixtures/tableFixture.ts create mode 100644 tests/fixtures/userconfig.js create mode 100644 tests/fixtures/wasteBookFixture.ts create mode 100644 tests/imgs/__screenshots__/touch/boss_wastebook.spec.js/流水-营业记录-单据明细-补签-1.png create mode 100644 tests/imgs/upload.jpg create mode 100644 tests/imgs/商品M.jpg create mode 100644 tests/pages/appointmentPage.ts create mode 100644 tests/pages/cashierRoomPage.ts create mode 100644 tests/pages/components/index.ts create mode 100644 tests/pages/components/numberInput.ts create mode 100644 tests/pages/customer/customerAnalysisPage.ts create mode 100644 tests/pages/customer/customerDetailsPage.ts create mode 100644 tests/pages/customer/customerPage.ts create mode 100644 tests/pages/customer/index.ts create mode 100644 tests/pages/goalPage.ts create mode 100644 tests/pages/homeNavigationPage.ts create mode 100644 tests/pages/inventory/InventoryManagementPage.ts create mode 100644 tests/pages/inventory/index.ts create mode 100644 tests/pages/inventory/inventoryTransferManagementPage.ts create mode 100644 tests/pages/marketing/index.ts create mode 100644 tests/pages/marketing/marketingInviteGuestsPage.ts create mode 100644 tests/pages/marketing/marketingPage.ts create mode 100644 tests/pages/report/cardBalanceChangeReport.ts create mode 100644 tests/pages/report/customerConsumptionAnalysisReport.ts create mode 100644 tests/pages/report/index.ts create mode 100644 tests/pages/report/itemSalesConsumptionAccessReport.ts create mode 100644 tests/pages/report/performanceDetailReport.ts create mode 100644 tests/pages/report/performanceSummaryReport.ts create mode 100644 tests/pages/report/reportPage.ts create mode 100644 tests/pages/report/salesCostSummaryReport.ts create mode 100644 tests/pages/report/spendingSummaryReport.ts create mode 100644 tests/pages/tablePage.ts create mode 100644 tests/pages/wastebook/index.ts create mode 100644 tests/pages/wastebook/wasteBookBusinessRecordPage.ts create mode 100644 tests/setup/boss_auth.setup.ts create mode 100644 tests/setup/staff_auth.setup.ts create mode 100644 tests/touch/boss_appointment.spec.ts create mode 100644 tests/touch/boss_cashier.spec.ts create mode 100644 tests/touch/boss_customer.spec.ts create mode 100644 tests/touch/boss_goal.spec.ts create mode 100644 tests/touch/boss_inventory.spec.ts create mode 100644 tests/touch/boss_marketing.spec.ts create mode 100644 tests/touch/boss_report.spec.ts create mode 100644 tests/touch/boss_wastebook.spec.ts create mode 100644 tests/touch/staff_goal.spec.ts create mode 100644 tests/touch/staff_inventory.spec.ts create mode 100644 tests/utils/customer.ts create mode 100644 tests/utils/indexedDBUtils.ts create mode 100644 tests/utils/utils.js create mode 100644 tsconfig.json diff --git a/.env b/.env new file mode 100644 index 0000000..f8c925d --- /dev/null +++ b/.env @@ -0,0 +1,22 @@ +QY_WECHAT_ROOTBOT_URL='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1c7d2bf1-eae9-4ee9-bdfb-b578e747f2dc' +BASE_URL = 'https://hlk.meiguanjia.net/' +boss_account= "17770720220" +boss_password= "a123456" +boss_account_2= "1770720220" +boss_password_2= "a123456" +staff1_account= "17770720221" +staff1_password= "a123456" +staff2_account= "17770720222" +staff2_password= "a123456" + +STOREH5_URL = 'https://hlk.meiguanjia.net/h5/#/?env=0&mid=14920&tv=qym&query=%257B%2522tab%2522%253A%2522MALL%2522%257D' + +WEB_AUTH_USERNAME='mgj' +WEB_AUTH_PASSWORD='mgj123456' +DOMAIN_URL = 'https://autotest.meiguanjia.net' +SERVER_PORT = 9901 +PROXY_SERVER_PORT = 9900 +LDAP_BindURL = 'ldap://ldapadmin.meiguanjia.net:389' +LDAP_bindDN = 'cn=admin,dc=meiguanjia,dc=net' +LDAP_bindUserDN = 'ou=person,dc=meiguanjia,dc=net' +LDAPbindPSW = '!qaz2Wsxyjy0102' \ No newline at end of file diff --git a/.env.staging b/.env.staging new file mode 100644 index 0000000..36d712f --- /dev/null +++ b/.env.staging @@ -0,0 +1,21 @@ +BASE_URL = 'https://stghlk.meiguanjia.net/' +QY_WECHAT_ROOTBOT_URL='https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=1c7d2bf1-eae9-4ee9-bdfb-b578e747f2dc' +#QY_WECHAT_ROOTBOT_URL='' +boss_account= "17770720220" +boss_password= "a123456" +staff1_account= "17770720221" +staff1_password= "a123456" +staff2_account= "17770720222" +staff2_password= "a123456" + + + +WEB_AUTH_USERNAME='mgj' +WEB_AUTH_PASSWORD='mgj123456' +DOMAIN_URL = 'https://autotest.meiguanjia.net' +SERVER_PORT = 9901 +PROXY_SERVER_PORT = 9900 +LDAP_BindURL = 'ldap://ldapadmin.meiguanjia.net:389' +LDAP_bindDN = 'cn=admin,dc=meiguanjia,dc=net' +LDAP_bindUserDN = 'ou=person,dc=meiguanjia,dc=net' +LDAPbindPSW = '!qaz2Wsxyjy0102' \ No newline at end of file diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..4685834 --- /dev/null +++ b/.env.test @@ -0,0 +1,7 @@ +BASE_URL = 'https://prestghlk.meiguanjia.net/' +boss_account= "17770720220" +boss_password= "a123456" +staff1_account= "17770720221" +staff1_password= "a123456" +staff2_account= "17770720222" +staff2_password= "a123456" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f98bc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +/\.auth/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +package-lock.json +/.idea/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..fa99e53 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "semi": true, + "singleQuote": true, + "quoteProps": "consistent", + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "proseWrap": "never", + "endOfLine": "lf", + "embeddedLanguageFormatting": "auto" +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..1ab7241 --- /dev/null +++ b/Readme.md @@ -0,0 +1,111 @@ +这是慧来客的自动化测试系统规范,此项目支持图像化界面管理测试case,自动化测试,流水线发布且支持报告发送和错误现场回溯。如有不懂请联系WQ||Co! +### 支持的能力 +- ✅图形化测试用例管理 +- ✅多环境支持测试 +- ✅多线程测试 +- ✅多角色测试 +- ✅测试报告生成 +- ✅错误现场还原跟踪 +- ✅测试过程录像 +- ✅测试报告推送到企业微信 +- ✅API接口测试 +- ❌API压力测试 + +### 目录结构 +```md +playwright_project/ +│ +├── .auth/ # 认证信息存放位置 +├── node_modules/ # 存放 npm 包 +├── playwright-report/ #测试报告存放目录 +├── package.json # 项目配置文件 +├── test-results/ #测试解过 +├── playwright.config.ts # Playwright 配置文件 +├── tests/ # 测试case文件存放目录 +│ ├── touch/ # 慧来客touch端case的存放 +│ │ ├── cashier_registration.spec.js # 挂单 +│ │ ├── cashier_collection.spec.js #收款 +│ │ └── ... +│ ├── mallh5/ # 慧来客客户端h5 +│ │ ├── Mine_card.spec.js # 会员卡查看 +│ │ └── ... +│ ├── business/ # 测试套件目录 +│ │ ├── Member_search.spec.js # 搜索会员 +│ │ └── ... +│ ├── fixtures/ # 测试公用套件 +│ │ ├── user.json # 用户数据 +│ │ └── cashier_set.json #收银数据设置 +│ └── utils/ # 工具函数目录 +│ ├── screenshotHelper.ts # 截图辅助函数 +│ └── ... +└── tests-examples/ # 测试例子参考目录 +│ └── ... +└── .env* # 环境变量存放位置,根据 process.env.NODE_ENV 读取不同的配置文件 +└── my-awesome-reporter.ts # 自定义报告生成器用于推送 +└── playwright.config* # 测试项目基础配置文件 +``` +[测试框架技术文档](https://playwright.nodejs.cn/docs/intro) +### 项目规范 +1. case的存放地址:/tests,所有的文件名应该让人看见名称知道case文件具体的含义,使用 当前测试的角色_case一级菜单菜单命名,如管理员身份的收银用例的文件命名应该为:cashier.spec.js。 +- 总部管理员 boss +- 门店员工 staff + +2. case的编写规范:二级菜单下使用test.describe用二级菜单名字做分组然后二级菜单下的具体功能点则放在使用每一个test做声明。每一个功能点以具体的测试用例命名,如:收银-挂单的例子如下。 +```javascript +//收银挂单测试 +test.describe('收银-挂单', () => { + //挂单 + test('挂单', async ({ page }) => { + await page.getByRole('button', { name: '开 单' }).click(); + await page.getByRole('button', { name: '1' }).click(); + await page.getByText('搜索').click(); + await page.getByLabel('icon: close').locator('svg').click(); + await page.locator('.top > .anticon').first().click(); + }); + + + //取单 + test('取单', async ({ page }) => { + await page.getByRole('button', { name: '开 单' }).click(); + await page.getByRole('button', { name: '1' }).click(); + await page.getByText('搜索').click(); + await page.getByLabel('icon: close').locator('svg').click(); + await page.locator('.top > .anticon').first().click(); + }); +}) +``` +3. 测试参数设置,项目启动会根据名 process.env.NODE_ENV 读取不同文件的配置.env.${环境变量}设置到当前启动线程中,启动命令中使用cross-env设置环境变量。 +4. 预设的npm命令和后期添加规则,命令命名为 作用-环境名 +```shell +npm run prepared #安装必要的测试框架依赖 + + +npm run codegen +npm run codegen-staging #启动staging录制 + +npm run ui #启动图形化管理界面 +npm run ui-staging + +npm run report #获取测试报告 + +npm run test #启动测试 全量测试 +npm run test-staging #启动staging环境测试 全量测试 + +npm run ui-server #启动服务器图形化管理界面服务 +npm run ui-server-staging #启动服务器图形化管理界面服务 +``` +5. 写case的过程中请尽量避免使用waitfor利用平台自身的等待机制 +### 测试case索引 +|cese名称|case地址|case名称|状态|最后时间|编写人| +|-|-|-|-|-|-| +|收银-挂单|touch/boo_cashier_registration.spec.js|cosavebill|✅|2024-08-16|co| +|收银-挂单|touch/boo_cashier_registration.spec.js|cosavebill|✅|2024-08-16|co| +|收银-挂单|touch/boss_cashier.spec.js|cosavebill|✅|2024-08-19|chenjihuang| +| | | | | | | +### 图形化管理工具 +点击 测试管理工具.bat 启动 + + + +### case地址 +[Mat用例点我打开](https://doc.weixin.qq.com/sheet/e3_AX8AngY-AOkP8BOfjAJQqCgCwFQQZ?scode=AKsAggcJABAAF0r0lLALgA3QZpAMU&tab=000001) \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..44fdeb0 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "automated-testing", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "ui": "npx playwright test --ui", + "ui-staging": "cross-env NODE_ENV=staging npx playwright test --ui", + "test": "npx playwright test --ui --headed" + }, + "repository": { + "type": "git", + "url": "https://git.meiguanjia.net/gitlab/hlk/automated-testing.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@faker-js/faker": "^8.4.1", + "@playwright/test": "^1.48.2", + "@types/node": "^22.3.0", + "axios": "^1.7.4", + "cross-env": "^7.0.3" + }, + "dependencies": { + "dotenv": "^16.4.5", + "express": "^4.19.2", + "express-session": "^1.18.0", + "http-proxy-middleware": "^3.0.0", + "jimp": "^0.22.12", + "ldap": "^0.7.1", + "ldap-authentication": "^3.2.2", + "ldapjs": "^3.0.7", + "playwright": "^1.47.2", + "qrcode-reader": "^1.0.4", + "sharp": "^0.33.5", + "tesseract.js": "^5.1.1", + "typescript": "^5.6.3" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..366ac72 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,136 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); +// import dotenv from "dotenv"; +import path from 'path'; +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +require('dotenv').config({ path: path.resolve(__dirname, '.env') }); + +// dotenv.config({ +// path: path.resolve( +// __dirname, +// ".env" + `${process.env.NODE_ENV ? "." + process.env.NODE_ENV : ""}` +// ), +// }); +const authPath = '.auth/user.json'; +const firstAuthFile = '.auth/admin_first.json'; +const secondAuthFile = '.auth/admin_second.json'; +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + name: '美管加自动测试框架', + timeout: 10 * 60 * 1000, + globalTimeout: 600 * 60 * 1000, + expect: { + timeout: 30 * 1000, + toPass: { + timeout: 30 * 1000, + }, + }, + testDir: './tests/', + snapshotPathTemplate: '{testDir}/imgs/__screenshots__/{testFilePath}/{arg}{ext}', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 1 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : 2, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html'], ['list'], ['./my-awesome-reporter.ts']], + + // reporter: './my-awesome-reporter.ts', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + actionTimeout: 30 * 1000, + navigationTimeout: 60 * 1000, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + hasTouch: true, + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + timezoneId: 'Asia/Shanghai', + locale: 'zh-CN', + geolocation: { longitude: 114.24, latitude: 22.73 }, + permissions: ['geolocation'], + extraHTTPHeaders: { + 'Accept-Language': 'zh-CN,zh;q=0.9', + }, + video: { + mode: 'retain-on-failure', + size: { width: 640, height: 480 }, + }, + }, + /* Configure projects for major browsers */ + projects: [ + { + name: '总部管理员鉴权', + testMatch: /.*boss_auth\.setup\.js/, + use: { baseURL: process.env.BASE_URL }, + }, + { + name: '门店员工鉴权', + testMatch: /.*staff_auth\.setup\.js/, + use: { baseURL: process.env.BASE_URL }, + }, + { + name: '慧来客touch(管理员身份) - Desktop Chrome', + use: { + ...devices['Desktop Chrome'], + baseURL: process.env.BASE_URL, + storageState: firstAuthFile, + viewport: { width: 1280, height: 720 }, + isMobile: true, + }, + testMatch: /.*boss_.*\.spec\.js/, + dependencies: ['总部管理员鉴权'], + }, + { + name: '慧来客touch(管理员身份) - Desktop Safari', + use: { + ...devices['Desktop Safari'], + baseURL: process.env.BASE_URL, + storageState: firstAuthFile, + viewport: { width: 1280, height: 720 }, + isMobile: true, + }, + testMatch: /.*boss_.*\.spec\.js/, + dependencies: ['总部管理员鉴权'], + }, + { + name: '慧来客touch(门店员工身份) - Desktop Chrome', + use: { + ...devices['Desktop Chrome'], + baseURL: process.env.BASE_URL, + viewport: { width: 1280, height: 720 }, + isMobile: false, + }, + testMatch: /.*staff_.*\.spec\.js/, + dependencies: ['门店员工鉴权'], + }, + { + name: '慧来客touch(门店员工身份) - Desktop Safari', + use: { + ...devices['Desktop Safari'], + baseURL: process.env.BASE_URL, + viewport: { width: 1280, height: 720 }, + isMobile: false, + }, + testMatch: /.*staff_.*\.spec\.js/, + dependencies: ['门店员工鉴权'], + }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/tests-examples/demo-faker.js b/tests-examples/demo-faker.js new file mode 100644 index 0000000..7c93609 --- /dev/null +++ b/tests-examples/demo-faker.js @@ -0,0 +1,14 @@ +import { faker } from '@faker-js/faker'; +// 参考文档 +// https://fakerjs.dev/guide/ + +// 生成手机号 +const randomPhoneNumber = faker.helpers.fromRegExp(/1[3-9][0-9]{8}/); +console.log(randomPhoneNumber); +// 生成名字 +const randomName = faker.person.fullName(); +console.log(randomName); +// 生成城市 +const randomCity = faker.location.city(); +console.log(randomCity); + diff --git a/tests-examples/demo-todo-app.spec.js b/tests-examples/demo-todo-app.spec.js new file mode 100644 index 0000000..e2eb87c --- /dev/null +++ b/tests-examples/demo-todo-app.spec.js @@ -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); +} diff --git a/tests/fixtures/appointmentFixture.ts b/tests/fixtures/appointmentFixture.ts new file mode 100644 index 0000000..d9bb639 --- /dev/null +++ b/tests/fixtures/appointmentFixture.ts @@ -0,0 +1,15 @@ +import { test as base } from '@playwright/test'; +import { AppointmentPage } from '@/pages/appointmentPage'; + +type MyFixture = { + appointmentPage: AppointmentPage; +}; + +export const test = base.extend({ + appointmentPage: async ({ page }, use) => { + const appointmentPage = new AppointmentPage(page); + await use(appointmentPage); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/fixtures/baseFixture.ts b/tests/fixtures/baseFixture.ts new file mode 100644 index 0000000..438e53a --- /dev/null +++ b/tests/fixtures/baseFixture.ts @@ -0,0 +1,81 @@ +import { test as base, expect } from '@playwright/test'; +import { HomeNavigation } from '@/pages/homeNavigationPage'; +import { writeIndexedDB } from '@/utils/indexedDBUtils'; +import { readFileSync } from 'fs'; + +type MyFixture = { + homeNavigation: HomeNavigation; + billSet: Set; +}; + +export const test = base.extend({ + page: async ({ page, baseURL }, use) => { + await page.addLocatorHandler(page.locator('.left_img > .close_btn > .close_icon > svg'), async () => { + await page.locator('.left_img > .close_btn > .close_icon > svg').click(); + await expect(page.locator('.versionModal_main_content')).toBeVisible(); + }); + + if (!baseURL) throw new Error('baseURL is required'); + await page.goto(baseURL); + // await page.getByRole('button', { name: /开\s单/ }).waitFor(); + + const mobileObject = await page.evaluate(() => { + return window.localStorage.getItem('hlk_touch_mobile'); + }); + if (!mobileObject && mobileObject !== 'null') { + throw new Error('localStorage does not contain boss_account or boss_account2'); + } + const mobileString = JSON.parse(mobileObject); + const bossAccount = process.env.boss_account || ''; + const bossAccount2 = process.env.boss_account2 || ''; + + let jsonData: []; + if (mobileString.val?.includes(bossAccount)) { + jsonData = JSON.parse(readFileSync('.auth/admin_first_indexeddb.json', 'utf-8')); + } else if (mobileString.val?.includes(bossAccount2)) { + jsonData = JSON.parse(readFileSync('.auth/admin_second_indexeddb.json', 'utf-8')); + } else { + throw new Error('localStorage does not contain boss_account or boss_account2'); + } + + await page.evaluate( + async ({ fnString, jsonData }) => { + // 在浏览器上下文中动态创建函数 + const writeIndexedDBFn = new Function('return ' + fnString)(); + + // 调用函数并传递参数 + return await writeIndexedDBFn(jsonData); + }, + { fnString: writeIndexedDB.toString(), jsonData }, + ); + // await page.reload(); + await page.waitForFunction( + async () => { + const databases = await indexedDB.databases(); // 获取所有数据库 + return databases.some(db => db.name === 'hlk_touch_test_init'); // 判断是否存在目标数据库 + }, + { timeout: 30000 }, // 设置超时时间,默认为 30 秒 + ); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + + await use(page); + }, + + /** + * 首页导航 + */ + homeNavigation: async ({ page }, use) => { + const homeNavigation = new HomeNavigation(page); + await use(homeNavigation); + }, + + /** + * 流水单据集合(无重复水单) + */ + billSet: async ({ page }, use) => { + const billSet = new Set(); + await use(billSet); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/fixtures/boss_common.ts b/tests/fixtures/boss_common.ts new file mode 100644 index 0000000..cbd8632 --- /dev/null +++ b/tests/fixtures/boss_common.ts @@ -0,0 +1,28 @@ +import { mergeTests, mergeExpects } from '@playwright/test'; +import { test as cashierTest } from '@/fixtures/cashierFixture'; +import { test as appointmentTest } from '@/fixtures/appointmentFixture'; +import { test as wasteBookTest } from '@/fixtures/wasteBookFixture'; +import { test as customerTest } from '@/fixtures/customerFixture'; +import { test as inventoryTest } from '@/fixtures/inventoryFixture'; +import { test as goalTest } from '@/fixtures/goalFixture'; +import { test as reportTest, expect as reportExpect } from '@/fixtures/reportFixture'; +import { test as marketingTest } from '@/fixtures/marketingFixture'; +import { test as baseTest } from '@/fixtures/baseFixture'; +import { test as tableTest } from '@/fixtures/tableFixture'; +import { test as componentsTest } from '@/fixtures/componentsFixture'; + +export const test = mergeTests( + cashierTest, + appointmentTest, + customerTest, + wasteBookTest, + inventoryTest, + goalTest, + reportTest, + baseTest, + marketingTest, + tableTest, + componentsTest, +); + +export const expect = mergeExpects(reportExpect); diff --git a/tests/fixtures/cashierFixture.ts b/tests/fixtures/cashierFixture.ts new file mode 100644 index 0000000..5bfb736 --- /dev/null +++ b/tests/fixtures/cashierFixture.ts @@ -0,0 +1,15 @@ +import { test as base } from '@playwright/test'; +import { CashierRoomPage } from '@/pages/cashierRoomPage.js'; + +type MyFixture = { + cashierRoomPage: CashierRoomPage; +}; + +export const test = base.extend({ + cashierRoomPage: async ({ page }, use) => { + const cashierRoomPage = new CashierRoomPage(page); + await use(cashierRoomPage); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/fixtures/componentsFixture.ts b/tests/fixtures/componentsFixture.ts new file mode 100644 index 0000000..4e75320 --- /dev/null +++ b/tests/fixtures/componentsFixture.ts @@ -0,0 +1,13 @@ +import { test as base } from '@playwright/test'; +import { NumberInput } from '@/pages/components'; + +type MyFixture = { + numberInput: NumberInput; +}; + +export const test = base.extend({ + numberInput: async ({ page }, use) => { + const numberInput = new NumberInput(page); + await use(numberInput); + }, +}); diff --git a/tests/fixtures/customerFixture.ts b/tests/fixtures/customerFixture.ts new file mode 100644 index 0000000..24742ae --- /dev/null +++ b/tests/fixtures/customerFixture.ts @@ -0,0 +1,148 @@ +import { test as base } from '@playwright/test'; +import { CustomerPage, CustomerDetailsPage, CustomerAnalysisPage } from '@/pages/customer'; +import { Customer } from '@/utils/customer'; + +type MyFixture = { + singleCustomerPage: { + createCustomer: (customer: Customer) => Promise; + setInvalidCustomer: (customer: Customer) => Promise; + }; + moreCustomerPage: { + createMoreCustomer: (customer: Customer[]) => Promise; + setMoreInvalidCustomer: (customer: Customer[]) => Promise; + }; + customerPage: CustomerPage; + customerDetailsPage: CustomerDetailsPage; + customerAnalysisPage: CustomerAnalysisPage; + createCustomer: Customer; + createCustomers: (customerNumber: number) => Promise; + createCustomCustomer: (customer: Customer) => Promise; + createCustomCustomers: (customers: Customer[]) => Promise; +}; + +export const test = base.extend({ + /** + * 创建一个顾客 + */ + createCustomer: async ({ page, baseURL }, use) => { + if (!baseURL) { + throw new Error('baseUrl is required'); + } + + const customerPage = new CustomerPage(page); + const customer = new Customer(1, 1); + await customerPage.createCustomer(customer); + await page.goto(baseURL); + await use(customer); + await page.goto(baseURL); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + await page.goto('/#/member/member-schame'); + await customerPage.setInvalidCustomer(customer); + }, + + /** + * 创建多个顾客 + */ + createCustomers: async ({ page, baseURL }, use): Promise => { + if (!baseURL) { + throw new Error('baseUrl is required'); + } + + const customerPage = new CustomerPage(page); + let createdCustomers: Customer[] = []; + + /** + * 创建多个顾客 + * @param customerNumber { number } 顾客数量 + * @returns { Promise } 顾客列表 + */ + const createCustomers = async (customerNumber: number): Promise => { + for (let i = 0; i < customerNumber; i++) { + const customer = new Customer(1, 1); + await customerPage.createCustomer(customer); + createdCustomers.push(customer); + } + + return createdCustomers; + }; + + await use(createCustomers); + + if (createdCustomers.length === 0) return; + + // 测试结束后将客户设置为无效 + await page.goto(baseURL); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + await page.goto('/#/member/member-schame'); + await customerPage.setMoreInvalidCustomer(createdCustomers); + }, + + /** + * 创建一个自定义顾客 + */ + createCustomCustomer: async ({ page, baseURL }, use) => { + if (!baseURL) { + throw new Error('baseUrl is required'); + } + + const customerPage = new CustomerPage(page); + let tmp: Customer | undefined; + const setCustomer = async (customer: Customer) => { + await customerPage.createCustomer(customer); + tmp = customer; + }; + await use(setCustomer); + + if (!tmp) return; + await page.goto(baseURL); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + await page.goto('/#/member/member-schame'); + await customerPage.setInvalidCustomer(tmp); + }, + + /** + * 创建多个自定义顾客 + */ + createCustomCustomers: async ({ page, baseURL }, use) => { + if (!baseURL) { + throw new Error('baseUrl is required'); + } + const customerPage = new CustomerPage(page); + let tmp: Customer[] | undefined; + const setCustomers = async (customers: Customer[]) => { + await customerPage.createMoreCustomer(customers); + tmp = customers; + }; + await use(setCustomers); + + if (!tmp) return; + await page.goto(baseURL); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + await page.goto('/#/member/member-schame'); + await customerPage.setMoreInvalidCustomer(tmp); + }, + + /** + * 顾客页面 + */ + customerPage: async ({ page }, use) => { + const customerPage = new CustomerPage(page); + await use(customerPage); + }, + + /** + * 顾客详情 + */ + customerDetailsPage: async ({ page }, use) => { + const customerDetailsPage = new CustomerDetailsPage(page); + await use(customerDetailsPage); + }, + + /** + * 顾客分析 + */ + customerAnalysisPage: async ({ page }, use) => { + const customerAnalysisPage = new CustomerAnalysisPage(page); + await use(customerAnalysisPage); + }, +}); diff --git a/tests/fixtures/goalFixture.ts b/tests/fixtures/goalFixture.ts new file mode 100644 index 0000000..6e79ac0 --- /dev/null +++ b/tests/fixtures/goalFixture.ts @@ -0,0 +1,13 @@ +import { test as base } from '@playwright/test'; +import { GoalPage } from '@/pages/goalPage'; + +type MyFixture = { goalPage: GoalPage }; + +export const test = base.extend({ + goalPage: async ({ page }, use) => { + const goalPage = new GoalPage(page); + await use(goalPage); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/fixtures/inventoryFixture.ts b/tests/fixtures/inventoryFixture.ts new file mode 100644 index 0000000..dfd2ac0 --- /dev/null +++ b/tests/fixtures/inventoryFixture.ts @@ -0,0 +1,21 @@ +import { test as base } from '@playwright/test'; +import { TransferManagementPage, InventoryManagementPage } from '@/pages/inventory'; + +type MyFixture = { + transferManagementPage: TransferManagementPage; + inventoryManagementPage: InventoryManagementPage; +}; + +export const test = base.extend({ + transferManagementPage: async ({ page }, use) => { + const transferManagementPage = new TransferManagementPage(page); + await use(transferManagementPage); + }, + + inventoryManagementPage: async ({ page }, use) => { + const inventoryManagementPage = new InventoryManagementPage(page); + await use(inventoryManagementPage); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/fixtures/marketingFixture.ts b/tests/fixtures/marketingFixture.ts new file mode 100644 index 0000000..2ee0256 --- /dev/null +++ b/tests/fixtures/marketingFixture.ts @@ -0,0 +1,18 @@ +import { test as base } from '@playwright/test'; +import { MarketingPage, MarketingInviteGuestsPage } from '@/pages/marketing'; + +type MyFixture = { + marketingPage: MarketingPage; + marketingInviteGuestsPage: MarketingInviteGuestsPage; +}; + +export const test = base.extend({ + marketingPage: async ({ page }, use) => { + const marketingPage = new MarketingPage(page); + await use(marketingPage); + }, + marketingInviteGuestsPage: async ({ page }, use) => { + const marketingInviteGuestsPage = new MarketingInviteGuestsPage(page); + await use(marketingInviteGuestsPage); + }, +}); diff --git a/tests/fixtures/merchantFixture.ts b/tests/fixtures/merchantFixture.ts new file mode 100644 index 0000000..28c874f --- /dev/null +++ b/tests/fixtures/merchantFixture.ts @@ -0,0 +1,52 @@ +import { test as base } from '@/fixtures/boss_common'; +import { BrowserContext, Page, TestInfo } from '@playwright/test'; +import path from 'path'; +import fs from 'fs'; + +// 定义商户数据类型 +type Merchant = { + id: string; + name: string; + storageState: string; +}; + +// 二维码能够解析出url +const authDirPath = path.resolve(__dirname, '../../.auth'); +if (!fs.existsSync(authDirPath)) { + fs.mkdirSync(authDirPath, { recursive: true }); +} + +const merchants: Merchant[] = [ + { + id: '14920', + name: '商户 1', + storageState: path.resolve(authDirPath, 'admin_first.json'), + }, + { + id: '14983', + name: '商户 2', + storageState: path.resolve(authDirPath, 'admin_second.json'), + }, +]; + +export const test = base.extend<{ + page: Page; +}>({ + page: async ({ browser, baseURL }, use, workerInfo: TestInfo) => { + // 根据 workerIndex 从商户池中获取对应商户 + const selectedMerchant = merchants[workerInfo.workerIndex % merchants.length]; + // const selectedMerchant = merchants[1]; + const context: BrowserContext = await browser.newContext({ + storageState: selectedMerchant.storageState, + }); + const page: Page = await context.newPage(); + console.log(`使用的商户: ${selectedMerchant.name}`); + await page.goto(baseURL || ''); + // 使用商户信息进行测试 + await use(page); + await page.close(); + await context.close(); + }, +}); + +export { expect } from './boss_common'; diff --git a/tests/fixtures/reportFixture.ts b/tests/fixtures/reportFixture.ts new file mode 100644 index 0000000..02d0194 --- /dev/null +++ b/tests/fixtures/reportFixture.ts @@ -0,0 +1,66 @@ +import { test as base, expect as baseExpect } from '@playwright/test'; +import { + ReportPage, + PerformanceSummaryReportPage, + SpendingSummaryReportPage, + PerformanceDetailReportPage, + CardBalanceChangeReportPage, + ItemSalesConsumptionAccessReportPage, + SalesCostSummaryReportPage, + CustomerConsumptionAnalysisReportPage, +} from '@/pages/report'; + +type MyFixture = { + reportPage: ReportPage; + performanceSummaryReportPage: PerformanceSummaryReportPage; + spendingSummaryReportPage: SpendingSummaryReportPage; + performanceDetailReportPage: PerformanceDetailReportPage; + cardBalanceChangeReportPage: CardBalanceChangeReportPage; + itemSalesConsumptionAccessReportPage: ItemSalesConsumptionAccessReportPage; + salesCostSummaryReportPage: SalesCostSummaryReportPage; + customerConsumptionAnalysisReportPage: CustomerConsumptionAnalysisReportPage; +}; + +export const test = base.extend({ + reportPage: async ({ page }, use) => { + const reportPage = new ReportPage(page); + await use(reportPage); + }, + + performanceSummaryReportPage: async ({ page }, use) => { + const performanceSummaryReportPage = new PerformanceSummaryReportPage(page); + await use(performanceSummaryReportPage); + }, + + performanceDetailReportPage: async ({ page }, use) => { + const performanceDetailReportPage = new PerformanceDetailReportPage(page); + await use(performanceDetailReportPage); + }, + + cardBalanceChangeReportPage: async ({ page }, use) => { + const cardBalanceChangeReportPage = new CardBalanceChangeReportPage(page); + await use(cardBalanceChangeReportPage); + }, + + spendingSummaryReportPage: async ({ page }, use) => { + const spendingSummaryReportPage = new SpendingSummaryReportPage(page); + await use(spendingSummaryReportPage); + }, + + itemSalesConsumptionAccessReportPage: async ({ page }, use) => { + const itemSalesConsumptionAccessReportPage = new ItemSalesConsumptionAccessReportPage(page); + await use(itemSalesConsumptionAccessReportPage); + }, + + salesCostSummaryReportPage: async ({ page }, use) => { + const salesCostSummaryReportPage = new SalesCostSummaryReportPage(page); + await use(salesCostSummaryReportPage); + }, + + customerConsumptionAnalysisReportPage: async ({ page }, use) => { + const customerConsumptionAnalysisReportPage = new CustomerConsumptionAnalysisReportPage(page); + await use(customerConsumptionAnalysisReportPage); + }, +}); + +export const expect = baseExpect.extend({}); diff --git a/tests/fixtures/staff.js b/tests/fixtures/staff.js new file mode 100644 index 0000000..f6a1238 --- /dev/null +++ b/tests/fixtures/staff.js @@ -0,0 +1,214 @@ +//@ts-check +// 默认为生产 +let nodeEnv = process.env.NODE_ENV || "production"; +nodeEnv = nodeEnv === "staging" ? "production" : nodeEnv; + +/** + * - 员工数据 门店 部门 员工 姓名 + * - staffData.firstStore.firstSector.employee_1.name + * - 员工数据 门店 部门 员工 手机号 + * - staffData.firstStore.firstSector.employee_1.phone + * - 员工数据 门店 部门 员工 ID + * - staffData.firstStore.firstSector.employee_1.id + */ +let staffData = { + firstStore: { + firstSector: { + name: "美容部", + employee_1: { + name: "张伟", + phone: "13812345678", + id: { production: 3, test: 1 }, + }, + employee_2: { + name: "李娜", + phone: "13987654321", + id: { production: 4, test: 2 }, + }, + employee_3: { + name: "王芳", + phone: "13723456789", + id: { production: 5, test: 3 }, + }, + employee_4: { + name: "陈刚", + phone: "13698765432", + id: { production: 6, test: 4 }, + }, + employee_5: { + name: "赵军", + phone: "13512349876", + id: { production: 7, test: 5 }, + }, + employee_6: { + name: "刘强", + phone: "13498761234", + id: { production: 8, test: 6 }, + }, + employee_7: { + name: "周萍", + phone: "13365432109", + id: { production: 9, test: 7 }, + }, + employee_8: { + name: "吴浩", + phone: "13287654329", + id: { production: 10, test: 8 }, + }, + employee_9: { + name: "徐亮", + phone: "13123459876", + id: { production: 11, test: 9 }, + }, + employee_10: { + name: "杨雪", + phone: "13098761234", + id: { production: 12, test: 10 }, + }, + }, + secondSector: { + name: "医美部", + employee_1: { + name: "赵伟", + phone: "13923456789", + id: { production: 13, test: 11 }, + }, + employee_2: { + name: "钱丽", + phone: "13898765432", + id: { production: 14, test: 12 }, + }, + employee_3: { + name: "孙峰", + phone: "13712349876", + id: { production: 15, test: 13 }, + }, + employee_4: { + name: "李涛", + phone: "13687654321", + id: { production: 16, test: 14 }, + }, + employee_5: { + name: "周慧", + phone: "13598761234", + id: { production: 17, test: 15 }, + }, + employee_6: { + name: "吴凯", + phone: "13465432109", + id: { production: 18, test: 16 }, + }, + employee_7: { + name: "郑翔", + phone: "13387654329", + id: { production: 19, test: 17 }, + }, + employee_8: { + name: "冯敏", + phone: "13223459876", + id: { production: 20, test: 18 }, + }, + employee_9: { + name: "朱强", + phone: "13198761234", + id: { production: 21, test: 19 }, + }, + employee_10: { + name: "何平", + phone: "13065432198", + id: { production: 22, test: 20 }, + }, + }, + }, + secondStore: { + name: "美容部", + firstSector: { + employee_1: { + name: "张凯", + phone: "13865432198", + id: { production: 1, test: 1 }, + }, + employee_2: { + name: "李军", + phone: "13923459876", + id: { production: 2, test: 2 }, + }, + employee_3: { + name: "王涛", + phone: "13798761234", + id: { production: 3, test: 3 }, + }, + employee_4: { + name: "陈敏", + phone: "13654321987", + id: { production: 4, test: 4 }, + }, + employee_5: { + name: "赵峰", + phone: "13523456789", + id: { production: 5, test: 5 }, + }, + employee_6: { + name: "刘丽", + phone: "13487654321", + id: { production: 6, test: 6 }, + }, + employee_7: { + name: "周亮", + phone: "13398765432", + id: { production: 7, test: 7 }, + }, + employee_8: { + name: "吴平", + phone: "13212349876", + id: { production: 8, test: 8 }, + }, + employee_9: { + name: "徐浩", + phone: "13165432109", + id: { production: 9, test: 9 }, + }, + employee_10: { + name: "孙杰", + phone: "13087654329", + id: { production: 10, test: 10 }, + }, + }, + }, +}; + +/** + * + * @param {Object} staffData + * @param {string} nodeEnv + * @returns + */ +function init(staffData, nodeEnv) { + const updatedData = JSON.parse(JSON.stringify(staffData)); // 深拷贝对象,避免修改原数据 + + function updateIds(sector) { + Object.keys(sector).forEach(key => { + const employee = sector[key]; + if (employee && employee.id && employee.id[nodeEnv] !== undefined) { + employee.id = employee.id[nodeEnv]; // 将 id 替换为 production 或 test 的值 + } + }); + } + + // 遍历 firstStore 和 secondStore 下的各个 sector,更新员工的 id + Object.keys(updatedData).forEach(storeKey => { + const store = updatedData[storeKey]; + Object.keys(store).forEach(sectorKey => { + const sector = store[sectorKey]; + if (typeof sector === "object" && sector !== null && sector.name) { + updateIds(sector); // 更新每个 sector 的员工 id + } + }); + }); + + return updatedData; // 返回更新后的数据 +} + +staffData = init(staffData, nodeEnv); + +export { staffData }; diff --git a/tests/fixtures/staffFixture.ts b/tests/fixtures/staffFixture.ts new file mode 100644 index 0000000..a3329ff --- /dev/null +++ b/tests/fixtures/staffFixture.ts @@ -0,0 +1,46 @@ +import { test as base } from '@playwright/test'; +import { HomeNavigation } from '@/pages/homeNavigationPage.js'; +import { TransferManagementPage } from '@/pages/inventory'; + +export const test = base.extend({ + /** + * @type { import("@playwright/test").Page } + */ + firstStaffPage: async ({ browser }, use) => { + const context = await browser.newContext({ + storageState: '.auth/user_1.json', + }); + const page = await context.newPage(); + await page.goto(process.env.BASE_URL ?? ''); + await use(page); + await context.close(); + }, + /** + * @type { import("@playwright/test").Page } + */ + secondStaffPage: async ({ browser }, use) => { + const context = await browser.newContext({ + storageState: '.auth/user_2.json', + }); + const page = await context.newPage(); + await page.goto(process.env.BASE_URL ?? ''); + await use(page); + await context.close(); + }, + /** + * @type { HomeNavigation } + */ + staffHomeNavigation: async ({ firstStaffPage }, use) => { + const homeNavigation = new HomeNavigation(firstStaffPage); + await use(homeNavigation); + }, + /** + * @type { TransferManagementPage } + */ + transferManagementPage: async ({ firstStaffPage }, use) => { + const transferManagementPage = new TransferManagementPage(firstStaffPage); + await use(transferManagementPage); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/tests/fixtures/staff_common.ts b/tests/fixtures/staff_common.ts new file mode 100644 index 0000000..b02e82a --- /dev/null +++ b/tests/fixtures/staff_common.ts @@ -0,0 +1,11 @@ +import { mergeTests } from '@playwright/test'; +import { test as cashierTest } from '@/fixtures/cashierFixture'; +import { test as appointmentTest } from '@/fixtures/appointmentFixture'; +import { test as inventoryTest } from '@/fixtures/inventoryFixture'; +import { test as staffTest } from '@/fixtures/staffFixture'; +import { test as baseTest } from '@/fixtures/baseFixture'; +import { test as componentsTest } from '@/fixtures/componentsFixture'; + +export const test = mergeTests(cashierTest, appointmentTest, inventoryTest, staffTest, baseTest, componentsTest); + +export { expect } from '@playwright/test'; diff --git a/tests/fixtures/tableFixture.ts b/tests/fixtures/tableFixture.ts new file mode 100644 index 0000000..b22516a --- /dev/null +++ b/tests/fixtures/tableFixture.ts @@ -0,0 +1,9 @@ +import { test as base } from '@playwright/test'; +import { TablePage } from '@/pages/tablePage'; + +export const test = base.extend<{ tablePage: TablePage }>({ + tablePage: async ({ page }, use) => { + const tablePage = new TablePage(page); + await use(tablePage); + }, +}); diff --git a/tests/fixtures/userconfig.js b/tests/fixtures/userconfig.js new file mode 100644 index 0000000..c7761e9 --- /dev/null +++ b/tests/fixtures/userconfig.js @@ -0,0 +1,1020 @@ +// 会员信息 +export const Member = { + //测试会员 + MemName_0: { + name: "测试会员", + PhoneNum: "1112223334", + }, + // 陈世美 + MemName_1: { + name: "陈世美", + PhoneNum: "15099903112", + }, + // 潘金莲 + MemName_2: { + name: "潘金莲", + PhoneNum: "15099903111", + }, + // 武大郎 + MemName_3: { + name: "武大郎", + PhoneNum: "15099903110", + }, + MemName_4: { + name: "随机会员", + PhoneNum: "15099903110", + }, +}; +// 常见字符 +export const Feature = { + // 门店 + Store: { + Store_1: { + name: "AT测试一店", + }, + Store_2: { + name: "AT测试二店", + }, + }, +}; +// 员工 +export const Employees = { + FirstShop: { + Employee_1: { + name: "一店顾问001", + num: "1", + }, + Employee_2: { + name: "一店管理员", + num: "2", + }, + Employee_3: { + name: "张伟", + num: "3", + }, + Employee_4: { + name: "李娜", + num: "4", + }, + Employee_5: { + name: "王芳", + num: "5", + }, + Employee_6: { + name: "陈刚", + num: "6", + }, + Employee_7: { + name: "赵军", + num: "7", + }, + Employee_8: { + name: "刘强", + num: "8", + }, + Employee_9: { + name: "周萍", + num: "9", + }, + Employee_10: { + name: "吴浩", + num: "10", + }, + Employee_11: { + name: "徐亮", + num: "11", + }, + Employee_12: { + name: "杨雪", + num: "12", + }, + Employee_13: { + name: "赵伟", + num: "13", + }, + Employee_14: { + name: "钱丽", + num: "14", + }, + Employee_15: { + name: "孙峰", + num: "15", + }, + Employee_16: { + name: "李涛", + num: "16", + }, + Employee_17: { + name: "周慧", + num: "17", + }, + Employee_18: { + name: "吴凯", + num: "18", + }, + Employee_19: { + name: "郑翔", + num: "19", + }, + Employee_20: { + name: "冯敏", + num: "20", + }, + Employee_21: { + name: "朱强", + num: "21", + }, + Employee_22: { + name: "何平", + num: "22", + }, + }, + SecondShop: { + // 二店员工 + Employee_1: { + name: "张凯", + num: "1", + }, + Employee_2: { + name: "李军", + num: "2", + }, + Employee_3: { + name: "王涛", + num: "3", + }, + Employee_4: { + name: "陈敏", + num: "4", + }, + Employee_5: { + name: "赵峰", + num: "5", + }, + Employee_6: { + name: "刘丽", + num: "6", + }, + Employee_7: { + name: "周亮", + num: "7", + }, + Employee_8: { + name: "吴平", + num: "8", + }, + Employee_9: { + name: "徐浩", + num: "9", + }, + Employee_10: { + name: "孙杰", + num: "10", + }, + }, +}; +// 项目名称: +export const ProjectName = { + //项目 + Projects: { + Projects_1: { num: "100012", name: "雪肌晶纯护理", Price: "300" }, + Projects_2: { num: "100013", name: "净透驻氧护理", Price: "380" }, + Projects_3: { num: "100014", name: "眼部神采飞扬护理", Price: "1980" }, + Projects_4: { num: "100015", name: "1苹果精萃护理", Price: "12800" }, + Projects_5: { num: "100016", name: "1钒钛中胚护理", Price: "1280" }, + Projects_6: { num: "100017", name: "钒钛微晶无针水光护理", Price: "10800" }, + Projects_7: { num: "100018", name: "苹果精萃护理", Price: "980" }, + Projects_8: { num: "100019", name: "青春焕活套", Price: "880" }, + Projects_9: { num: "100020", name: "青春雅致套", Price: "1980" }, + Projects_10: { num: "100021", name: "NSC抗衰", Price: "16900" }, + Projects_11: { num: "100022", name: "NSC抗衰+美白", Price: "23900" }, + Projects_12: { num: "100023", name: "七维", Price: "9800" }, + Projects_13: { num: "100024", name: "芳华时光精致护理", Price: "890" }, + Projects_14: { num: "100025", name: "铂瑞莱首次局部体验", Price: "399" }, + Projects_15: { num: "100026", name: "星灿明眸奢享套", Price: "100" }, + Projects_16: { num: "100027", name: "铂瑞莱高端部位抗衰", Price: "980" }, + Projects_17: { num: "100028", name: "净颜塑形尊贵套", Price: "100" }, + Projects_18: { num: "100029", name: "净颜水润美肌套", Price: "200" }, + Projects_19: { num: "100030", name: "水娃娃", Price: "248" }, + Projects_20: { num: "100031", name: "德国原装水元素", Price: "335" }, + Projects_21: { num: "100032", name: "德国原装冰吻之恋", Price: "498" }, + Projects_22: { num: "100033", name: "德国原装干细胞", Price: "1100" }, + Projects_23: { num: "100034", name: "细胞透析水漾悦肤疗程", Price: "498" }, + Projects_24: { num: "100035", name: "细胞透析青春紧致疗程", Price: "498" }, + Projects_25: { num: "100036", name: "眼部滋润去皱", Price: "210" }, + Projects_26: { num: "100037", name: "玉颈无痕", Price: "125" }, + Projects_27: { num: "100038", name: "纤纤玉手", Price: "160" }, + Projects_28: { num: "100039", name: "柔芙美1", Price: "398" }, + Projects_29: { num: "100040", name: "小气泡", Price: "88" }, + Projects_30: { num: "100041", name: "晶钻套", Price: "680" }, + Projects_31: { num: "100042", name: "圣斯丹雅美白套", Price: "0" }, + Projects_32: { num: "100043", name: "阿伴抗衰仪", Price: "880" }, + Projects_33: { num: "100044", name: "眼部抗衰微雕仪", Price: "380" }, + Projects_34: { num: "100045", name: "玛莉香园眼膜", Price: "0" }, + Projects_35: { num: "100046", name: "玛莉香园眼部", Price: "0" }, + Projects_36: { num: "100047", name: "EB透析", Price: "0" }, + Projects_37: { num: "100048", name: "除皱", Price: "9800" }, + Projects_38: { num: "100049", name: "轮廓", Price: "9800" }, + Projects_39: { num: "100050", name: "玛莉香园EB", Price: "0" }, + Projects_40: { num: "100051", name: "科技美容", Price: "3300" }, + Projects_41: { num: "100052", name: "四季仪", Price: "398" }, + Projects_42: { num: "100053", name: "嘉耀雅伦.青春套", Price: "9800" }, + Projects_43: { num: "100054", name: "四水灌肤", Price: "0" }, + Projects_44: { num: "100055", name: "皮膜修复", Price: "158" }, + Projects_45: { num: "100056", name: "正美汇净颜祛黄", Price: "1280" }, + Projects_46: { num: "100057", name: "正美汇水润baby", Price: "1680" }, + Projects_47: { num: "100058", name: "正美汇UV精致抗衰", Price: "5800" }, + Projects_48: { num: "100059", name: "正美汇(天鹅颈)", Price: "6800" }, + Projects_49: { num: "100060", name: "正美汇UV全能王", Price: "9800" }, + Projects_50: { num: "100061", name: "正美汇活萃", Price: "0" }, + Projects_51: { num: "100062", name: "魅宣", Price: "88" }, + Projects_52: { num: "100063", name: "嘉顿 1沙龙", Price: "198" }, + Projects_53: { num: "100064", name: "嘉顿 水氧", Price: "298" }, + Projects_54: { num: "100065", name: "嘉顿 变脸", Price: "398" }, + Projects_55: { num: "100066", name: "依贞", Price: "298" }, + Projects_56: { num: "100067", name: "半永久眉眼(代金卷2000)", Price: "2000" }, + Projects_57: { num: "100068", name: "轻氧净透毛孔清洁", Price: "198" }, + Projects_58: { num: "100069", name: "深水炸弹", Price: "3980" }, + Projects_59: { num: "100070", name: "樱花眼部", Price: "3980" }, + Projects_60: { num: "100071", name: "奥滨", Price: "1980" }, + Projects_61: { num: "100072", name: "中胚层(嘉耀", Price: "9800" }, + Projects_62: { num: "100073", name: "奇草眼雕", Price: "1580" }, + Projects_63: { num: "100074", name: "克莱士", Price: "158" }, + Projects_64: { num: "100075", name: "汎歌", Price: "380" }, + Projects_65: { num: "100076", name: "山水月花(1)", Price: "398" }, + Projects_66: { num: "100077", name: "科丽妍-祛斑", Price: "816" }, + Projects_67: { num: "100078", name: "科丽妍-点疣", Price: "100" }, + Projects_68: { num: "100079", name: "卡瑞姿", Price: "398" }, + Projects_69: { num: "100080", name: "伊斯贝拉眼球排毒套", Price: "398" }, + Projects_70: { num: "100081", name: "青雨", Price: "498" }, + Projects_71: { num: "100082", name: "韵姿", Price: "0" }, + Projects_72: { num: "100083", name: "台州韩派", Price: "5000" }, + Projects_73: { num: "100084", name: "眉毛", Price: "0" }, + Projects_74: { num: "100085", name: "漂唇", Price: "1980" }, + Projects_75: { num: "100086", name: "胶原灌注", Price: "598" }, + Projects_76: { num: "100087", name: "益生菌活肤系列", Price: "598" }, + Projects_77: { num: "100088", name: "臻皙净颜(祛黄)", Price: "698" }, + Projects_78: { num: "100089", name: "4D柔极光", Price: "380" }, + Projects_79: { num: "100090", name: "减法清洁术", Price: "880" }, + Projects_80: { num: "100091", name: "乳酸菌舒压活肤系列", Price: "698" }, + Projects_81: { num: "100092", name: "草本天然净肤系列", Price: "698" }, + Projects_82: { num: "100093", name: "暗疮预防修护系列", Price: "698" }, + Projects_83: { num: "100094", name: "女神玫瑰嫩肌系列", Price: "698" }, + Projects_84: { num: "100095", name: "四大系列颈部护理", Price: "398" }, + Projects_85: { num: "100096", name: "黄金焕肤", Price: "598" }, + Projects_86: { num: "100097", name: "红酒焕肤", Price: "598" }, + Projects_87: { num: "100098", name: "磁石焕肤", Price: "398" }, + Projects_88: { num: "100099", name: "眼部抗衰", Price: "398" }, + Projects_89: { num: "100100", name: "意肤", Price: "598" }, + Projects_90: { num: "100101", name: "微晶雪肤水润系列", Price: "880" }, + Projects_91: { num: "100102", name: "能量活颜", Price: "2980" }, + Projects_92: { num: "100103", name: "清肌活肤", Price: "1880" }, + Projects_93: { num: "100104", name: "赋活因子", Price: "2980" }, + Projects_94: { num: "100105", name: "清颜净透", Price: "2980" }, + Projects_95: { num: "100106", name: "极光亮颜", Price: "1880" }, + Projects_96: { num: "100107", name: "照光", Price: "198" }, + Projects_97: { num: "100108", name: "眼部护理", Price: "398" }, + Projects_98: { num: "100109", name: "无针水光", Price: "880" }, + Projects_99: { num: "100110", name: "水光皇后", Price: "1880" }, + Projects_100: { num: "100111", name: "全能", Price: "1280" }, + Projects_101: { num: "100112", name: "草莓鼻清洁", Price: "68" }, + Projects_102: { num: "100113", name: "蜂巢皮秒全脸平光嫩服", Price: "0" }, + Projects_103: { num: "100114", name: "超声刀美人线提升", Price: "0" }, + Projects_104: { num: "100115", name: "水漾", Price: "598" }, + Projects_105: { num: "100116", name: "美胸皇后", Price: "598" }, + Projects_106: { num: "100117", name: "光子嫩肤", Price: "3800" }, + Projects_107: { num: "100118", name: "焕颜仪", Price: "1280" }, + Projects_108: { num: "100119", name: "德茉1", Price: "598" }, + Projects_109: { num: "100120", name: "碎碎冰", Price: "598" }, + Projects_110: { num: "100121", name: "加法", Price: "598" }, + Projects_111: { num: "100122", name: "消脂针", Price: "0" }, + Projects_112: { num: "100123", name: "正美汇高定卡(1紧致)", Price: "0" }, + Projects_113: { num: "100124", name: "正美汇高定卡(轮廓重组)", Price: "0" }, + Projects_114: { num: "100125", name: "正美汇高定卡(胶原修复)", Price: "0" }, + Projects_115: { num: "100126", name: "正美汇高定卡(焕颜美肌)", Price: "0" }, + Projects_116: { num: "100127", name: "正美汇高定卡(定制去皱)", Price: "0" }, + Projects_117: { num: "100128", name: "正美汇高定卡(1抗衰)", Price: "0" }, + Projects_118: { num: "100129", name: "正美汇高定卡(眼部抗衰)", Price: "0" }, + Projects_119: { num: "100130", name: "正美汇高定卡(颈部抗衰)", Price: "0" }, + Projects_120: { num: "100131", name: "真美汇", Price: "0" }, + Projects_121: { num: "100132", name: "嘉耀美丽", Price: "128000" }, + Projects_122: { num: "100133", name: "葆蒂兰1", Price: "0" }, + Projects_123: { num: "100134", name: "葆蒂兰眼部", Price: "0" }, + Projects_124: { num: "100135", name: "芙因调肤", Price: "1980" }, + Projects_125: { num: "100136", name: "锐光美肌拉脸", Price: "1980" }, + Projects_126: { num: "100137", name: "梦露", Price: "2000" }, + Projects_127: { num: "100138", name: "亮彩醒肤护理套", Price: "598" }, + Projects_128: { num: "100139", name: "眼部高端仪抗衰", Price: "1980" }, + Projects_129: { num: "100140", name: "皮肤检测", Price: "380" }, + Projects_130: { num: "100141", name: "光子嫩肤", Price: "380" }, + Projects_131: { num: "100142", name: "全脸超皮秒祛斑", Price: "3800" }, + Projects_132: { num: "100143", name: "少女肌水baby", Price: "1980" }, + Projects_133: { num: "100144", name: "圣莉斯歌眼", Price: "0" }, + Projects_134: { num: "100145", name: "法式抗污染", Price: "238" }, + Projects_135: { num: "100146", name: "CY仪器", Price: "398" }, + Projects_136: { num: "100147", name: "细胞重构", Price: "398" }, + Projects_137: { num: "100148", name: "CY眼刮", Price: "398" }, + Projects_138: { num: "100149", name: "透析", Price: "88" }, + Projects_139: { num: "100150", name: "水灵动人", Price: "128" }, + Projects_140: { num: "100151", name: "注氧", Price: "198" }, + Projects_141: { num: "100152", name: "cy量子", Price: "598" }, + Projects_142: { num: "100153", name: "原生赋活护理", Price: "498" }, + Projects_143: { num: "100154", name: "注颜美肤护理", Price: "380" }, + Projects_144: { num: "100155", name: "净颜嫩肤", Price: "680" }, + Projects_145: { num: "100156", name: "5D冻龄护理", Price: "980" }, + Projects_146: { num: "100157", name: "私人订制皮肤管理", Price: "580" }, + Projects_147: { num: "100158", name: "明眸SPA水疗", Price: "580" }, + Projects_148: { num: "100159", name: "光子嫩肤", Price: "5800" }, + Projects_149: { num: "100160", name: "无创祛斑", Price: "5800" }, + Projects_150: { num: "100161", name: "纹绣", Price: "0" }, + Projects_151: { num: "100162", name: "肽肽针卡", Price: "500" }, + Projects_152: { num: "100163", name: "透明质酸套", Price: "4130" }, + Projects_153: { num: "100164", name: "eb干细胞套", Price: "7980" }, + Projects_154: { num: "100165", name: "eb维生素套", Price: "3356" }, + Projects_155: { num: "100166", name: "eb保湿套", Price: "3736" }, + Projects_156: { num: "100167", name: "eb植粹套", Price: "4280" }, + Projects_157: { num: "100168", name: "eb眼部套", Price: "2538" }, + Projects_158: { num: "100169", name: "轻奢贵族鲜活外泌体", Price: "1980" }, + Projects_159: { num: "100170", name: "焕颜童颜粉调理", Price: "0" }, + Projects_160: { num: "100171", name: "1微调", Price: "0" }, + Projects_161: { num: "100172", name: "赫本v雕", Price: "19800" }, + Projects_162: { num: "100173", name: "外泌体", Price: "3900" }, + Projects_163: { num: "100176", name: "百合花原液", Price: "999" }, + Projects_164: { num: "100178", name: "水光", Price: "1800" }, + Projects_165: { num: "100179", name: "外泌体", Price: "0" }, + Projects_166: { num: "100180", name: "VVS原液", Price: "0" }, + Projects_167: { num: "100181", name: "瓷娃娃", Price: "0" }, + Projects_168: { num: "100182", name: "依贞", Price: "0" }, + Projects_169: { num: "100183", name: "冰美人", Price: "0" }, + Projects_170: { num: "100184", name: "AOS美容卡", Price: "0" }, + Projects_171: { num: "100185", name: "木乃伊", Price: "0" }, + Projects_172: { num: "100186", name: "四层清洁", Price: "0" }, + Projects_173: { num: "100187", name: "青春水润", Price: "0" }, + Projects_174: { num: "100188", name: "美目传奇眼部", Price: "0" }, + Projects_175: { num: "100189", name: "眼部", Price: "0" }, + Projects_176: { num: "100190", name: "172变", Price: "0" }, + Projects_177: { num: "100191", name: "4980皮肤管理年卡", Price: "0" }, + Projects_178: { num: "100192", name: "水氧", Price: "0" }, + Projects_179: { num: "100193", name: "樱花焕肤", Price: "0" }, + Projects_180: { num: "100194", name: "路乐", Price: "0" }, + Projects_181: { num: "100195", name: "1瑜伽", Price: "0" }, + Projects_182: { num: "100197", name: "药膜", Price: "0" }, + Projects_183: { num: "100198", name: "欧兰丽露", Price: "0" }, + Projects_184: { num: "100199", name: "清洁", Price: "0" }, + Projects_185: { num: "100200", name: "蛋白膜", Price: "0" }, + Projects_186: { num: "100201", name: "除皱", Price: "0" }, + Projects_187: { num: "100202", name: "调肤", Price: "0" }, + Projects_188: { num: "100203", name: "瑞夫人", Price: "0" }, + Projects_189: { num: "100204", name: "灌肤", Price: "0" }, + Projects_190: { num: "100205", name: "美娜朵眼部", Price: "2380" }, + Projects_191: { num: "100206", name: "美娜朵眼部精致护理套", Price: "2380" }, + Projects_192: { num: "100207", name: "活氧换肤", Price: "1980" }, + Projects_193: { num: "100208", name: "醒肤灯泡肌", Price: "598" }, + Projects_194: { num: "100209", name: "苹果干细胞", Price: "1680" }, + Projects_195: { num: "100210", name: "七维局部抗衰", Price: "1980" }, + Projects_196: { num: "100211", name: "定制项目", Price: "298" }, + Projects_197: { num: "100212", name: "皮膜修复", Price: "398" }, + Projects_198: { num: "100213", name: "中胚层护理", Price: "1280" }, + Projects_199: { num: "100214", name: "眼部护理", Price: "398" }, + Projects_200: { num: "100215", name: "脱唇毛", Price: "1280" }, + Projects_201: { num: "100216", name: "美塑射频", Price: "0" }, + Projects_202: { num: "100217", name: "水疗", Price: "0" }, + Projects_203: { num: "100218", name: "高科技1抗衰", Price: "0" }, + Projects_204: { num: "100219", name: "逆龄针", Price: "0" }, + Projects_205: { num: "100220", name: "侧颜轮廓提升", Price: "0" }, + Projects_206: { num: "100221", name: "中胚美塑", Price: "0" }, + Projects_207: { num: "100222", name: "苗药", Price: "0" }, + Projects_208: { num: "100223", name: "记希莱蓝铜肽", Price: "0" }, + Projects_209: { num: "100224", name: "记希莱水润素", Price: "1990" }, + Projects_210: { num: "100225", name: "记希莱冻龄素", Price: "0" }, + Projects_211: { num: "100226", name: "记希莱修复软黄金", Price: "699" }, + Projects_212: { num: "100227", name: "代言礼:美娜朵醒肤护理", Price: "598" }, + Projects_213: { num: "100228", name: "美娜朵泗水", Price: "198" }, + Projects_214: { num: "100229", name: "泗水注氧", Price: "198" }, + Projects_215: { num: "100230", name: "眼球SPA", Price: "9999" }, + Projects_216: { num: "100231", name: "1肌底深层清洁", Price: "298" }, + Projects_217: { num: "100232", name: "记希莱光感精华", Price: "699" }, + Projects_218: { num: "100233", name: "记希莱水润素", Price: "1990" }, + Projects_219: { num: "100234", name: "记系莱修复软黄金", Price: "699" }, + Projects_220: { num: "100235", name: "记希莱三型胶原", Price: "99" }, + Projects_221: { num: "100236", name: "小针1单部位抗衰", Price: "980" }, + Projects_222: { num: "100237", name: "皮肤管理", Price: "999" }, + Projects_223: { num: "100238", name: "法国美娜朵净透注氧", Price: "760" }, + Projects_224: { num: "100239", name: "玻尿酸", Price: "398" }, + Projects_225: { num: "100240", name: "360°眼部精细管理", Price: "398" }, + Projects_226: { num: "100241", name: "醒肤灯泡肌护理", Price: "598" }, + Projects_227: { num: "100242", name: "圣莉斯歌1护理", Price: "380" }, + Projects_228: { num: "100243", name: "胶原水光", Price: "0" }, + Projects_229: { num: "100244", name: "毛孔净化管理", Price: "380" }, + Projects_230: { num: "100245", name: "屏障修护管理", Price: "198" }, + Projects_231: { num: "100246", name: "胶原灌肌管理", Price: "198" }, + Projects_232: { num: "100247", name: "法式水润管理", Price: "320" }, + Projects_233: { num: "100248", name: "体验超皮秒", Price: "1980" }, + Projects_234: { num: "100249", name: "韩国GCS", Price: "980" }, + Projects_235: { num: "100251", name: "玻色因复配精华", Price: "1980" }, + Projects_236: { num: "100255", name: "表皮水光", Price: "598" }, + Projects_237: { num: "100256", name: "淋巴青春启动套", Price: "0" }, + Projects_238: { num: "100257", name: "七维", Price: "9800" }, + Projects_239: { num: "100258", name: "出水芙蓉SPA水疗", Price: "380" }, + Projects_240: { num: "100259", name: "妮娜公主全身舒活", Price: "580" }, + Projects_241: { num: "100260", name: "6D2仪器护理", Price: "1280" }, + Projects_242: { num: "100261", name: "天鹅颈舒享紧致组合", Price: "0" }, + Projects_243: { num: "100262", name: "魔力胸舒享弹润组合", Price: "0" }, + Projects_244: { num: "100263", name: "天鹅颈法姿纯活力净畅套", Price: "3800" }, + Projects_245: { num: "100264", name: "魔力胸法姿纯胸部胶原套", Price: "3800" }, + Projects_246: { num: "100265", name: "铂瑞莱首次局部体验", Price: "399" }, + Projects_247: { num: "100266", name: "三重时光肌密(胸)", Price: "0" }, + Projects_248: { num: "100267", name: "三重时光肌盈(臀)", Price: "0" }, + Projects_249: { num: "100270", name: "铂瑞莱高端部位抗衰", Price: "1980" }, + Projects_250: { num: "100271", name: "头疗", Price: "0" }, + Projects_251: { num: "100272", name: "净本", Price: "0" }, + Projects_252: { num: "100273", name: "体制药浴", Price: "198" }, + Projects_253: { num: "100274", name: "暖宫", Price: "398" }, + Projects_254: { num: "100275", name: "圣斯丹雅特项", Price: "0" }, + Projects_255: { num: "100276", name: "圣斯丹雅排", Price: "0" }, + Projects_256: { num: "100277", name: "圣斯丹雅芦荟灌肤", Price: "100" }, + Projects_257: { num: "100278", name: "和悦套", Price: "298" }, + Projects_258: { num: "100279", name: "御之能排酸", Price: "298" }, + Projects_259: { num: "100280", name: "增生普乐(增生、结节)", Price: "398" }, + Projects_260: { num: "100281", name: "私人定制", Price: "98000" }, + Projects_261: { num: "100282", name: "净灵排毒", Price: "398" }, + Projects_262: { num: "100283", name: "博恩", Price: "1360" }, + Projects_263: { num: "100284", name: "(18支)绚丽紧致组合", Price: "3980" }, + Projects_264: { num: "100285", name: "(54支)雅致组合", Price: "5680" }, + Projects_265: { num: "100286", name: "(90支)天姿组合", Price: "15800" }, + Projects_266: { num: "100287", name: "(60支)HPV", Price: "16800" }, + Projects_267: { num: "100288", name: "(78支)暖宫组合", Price: "16800" }, + Projects_268: { num: "100289", name: "(78支)紧致组合", Price: "17800" }, + Projects_269: { num: "100290", name: "(108支)抗衰组合", Price: "45800" }, + Projects_270: { num: "100291", name: "基植宝:如意(排诊方)", Price: "2380" }, + Projects_271: { num: "100292", name: "风恫宝:吉祥(风湿方)", Price: "2380" }, + Projects_272: { num: "100293", name: "肤舒宝:菩提(排毒方)", Price: "2380" }, + Projects_273: { num: "100294", name: "能植宝:八宝(能量方)", Price: "2380" }, + Projects_274: { num: "100295", name: "妇晗宝:莲花(妇科方)", Price: "3800" }, + Projects_275: { num: "100296", name: "八宝灸(阳气套)", Price: "2880" }, + Projects_276: { num: "100297", name: "菩提灸(关节套)", Price: "2180" }, + Projects_277: { num: "100298", name: "吉祥灸(肩颈套)", Price: "2180" }, + Projects_278: { num: "100299", name: "宝瓶灸(脾胃套)", Price: "2180" }, + Projects_279: { num: "100300", name: "莲花灸(妇科套)", Price: "2180" }, + Projects_280: { num: "100301", name: "瑶浴", Price: "248" }, + Projects_281: { num: "100303", name: "乳腺疏通", Price: "248" }, + Projects_282: { num: "100304", name: "卵巢保养", Price: "320" }, + Projects_283: { num: "100305", name: "腹部疏减", Price: "320" }, + Projects_284: { num: "100306", name: "腰肾保养", Price: "320" }, + Projects_285: { num: "100310", name: "雷娜塔胸部", Price: "398" }, + Projects_286: { num: "100311", name: "圣斯丹雅腺体", Price: "0" }, + Projects_287: { num: "100312", name: "2灸疗套", Price: "498" }, + Projects_288: { num: "100313", name: "九阳天灸", Price: "298" }, + Projects_289: { num: "100314", name: "太极肩颈", Price: "114" }, + Projects_290: { num: "100315", name: "玛莉香园颈膜", Price: "0" }, + Projects_291: { num: "100316", name: "玛莉香园红石榴", Price: "0" }, + Projects_292: { num: "100317", name: "气血调理", Price: "1960" }, + Projects_293: { num: "100318", name: "四季仪", Price: "398" }, + Projects_294: { num: "100319", name: "魔法生命仪", Price: "238" }, + Projects_295: { num: "100320", name: "体质汗下法", Price: "198" }, + Projects_296: { num: "100321", name: "体质针剂", Price: "298" }, + Projects_297: { num: "100322", name: "光子床", Price: "65" }, + Projects_298: { num: "100323", name: "舒活特臀部", Price: "0" }, + Projects_299: { num: "100324", name: "舒活特手臂", Price: "0" }, + Projects_300: { num: "100325", name: "舒活特腿部", Price: "0" }, + Projects_301: { num: "100326", name: "舒活特肚子", Price: "0" }, + Projects_302: { num: "100327", name: "舒活特泡澡", Price: "0" }, + Projects_303: { num: "100328", name: "九行圣泓乳房", Price: "2380" }, + Projects_304: { num: "100329", name: "九行圣泓上腹", Price: "1980" }, + Projects_305: { num: "100330", name: "九行圣泓中腹", Price: "2380" }, + Projects_306: { num: "100331", name: "九行圣泓下腹", Price: "2380" }, + Projects_307: { num: "100332", name: "九行圣泓肩颈", Price: "2380" }, + Projects_308: { num: "100333", name: "九行圣泓泡澡", Price: "2980" }, + Projects_309: { num: "100334", name: "传祖瑶", Price: "2380" }, + Projects_310: { num: "100335", name: "太极肩颈", Price: "2680" }, + Projects_311: { num: "100336", name: "元气滋养", Price: "3980" }, + Projects_312: { num: "100337", name: "全息爱护养生套", Price: "2380" }, + Projects_313: { num: "100338", name: "手部护理", Price: "88" }, + Projects_314: { num: "100339", name: "养翘臀", Price: "398" }, + Projects_315: { num: "100340", name: "尚赫", Price: "380" }, + Projects_316: { num: "100341", name: "暖宫", Price: "0" }, + Projects_317: { num: "100342", name: "正美汇蜜提美胸", Price: "6800" }, + Projects_318: { num: "100343", name: "正美汇极塑美体", Price: "9900" }, + Projects_319: { num: "100344", name: "路乐", Price: "398" }, + Projects_320: { num: "100345", name: "法拉奇", Price: "398" }, + Projects_321: { num: "100346", name: "净灵", Price: "398" }, + Projects_322: { num: "100347", name: "圣加仑", Price: "1150" }, + Projects_323: { num: "100348", name: "臻爱", Price: "398" }, + Projects_324: { num: "100349", name: "华香龙.上焦", Price: "268" }, + Projects_325: { num: "100350", name: "华香龙.中焦", Price: "268" }, + Projects_326: { num: "100351", name: "华香龙.下焦", Price: "338" }, + Projects_327: { num: "100352", name: "野姜", Price: "268" }, + Projects_328: { num: "100353", name: "艾灸", Price: "90" }, + Projects_329: { num: "100354", name: "俏元素", Price: "198" }, + Projects_330: { num: "100355", name: "清韵", Price: "199" }, + Projects_331: { num: "100356", name: "筋雕", Price: "298" }, + Projects_332: { num: "100357", name: "北海道暖宫", Price: "280" }, + Projects_333: { num: "100358", name: "日式单色美甲", Price: "60" }, + Projects_334: { num: "100359", name: "肠道排毒", Price: "3980" }, + Projects_335: { num: "100360", name: "瑶浴", Price: "1680" }, + Projects_336: { num: "100361", name: "九阳天灸宫廷养生", Price: "3980" }, + Projects_337: { num: "100362", name: "九阳天灸局部", Price: "2980" }, + Projects_338: { num: "100363", name: "经典胸部", Price: "3800" }, + Projects_339: { num: "100364", name: "太极肩颈", Price: "3650" }, + Projects_340: { num: "100365", name: "誉体韵", Price: "9800" }, + Projects_341: { num: "100366", name: "思禹SM", Price: "1980" }, + Projects_342: { num: "100367", name: "雷专SM", Price: "9800" }, + Projects_343: { num: "100368", name: "古方", Price: "2380" }, + Projects_344: { num: "100369", name: "山水月花全身淋巴大排毒", Price: "6800" }, + Projects_345: { num: "100370", name: "温骨", Price: "2980" }, + Projects_346: { num: "100371", name: "蓝氧spa", Price: "1980" }, + Projects_347: { num: "100372", name: "汪氏火罐", Price: "280" }, + Projects_348: { num: "100373", name: "体制泡澡", Price: "380" }, + Projects_349: { num: "100374", name: "时光机", Price: "0" }, + Projects_350: { num: "100375", name: "山水月花(2)", Price: "598" }, + Projects_351: { num: "100376", name: "巨邦套盒-纯净佳人", Price: "380" }, + Projects_352: { num: "100377", name: "巨邦套盒-活色生香", Price: "380" }, + Projects_353: { num: "100378", name: "巨邦套盒-粉黛佳人", Price: "580" }, + Projects_354: { num: "100379", name: "缔爱生-抑菌排毒套", Price: "425" }, + Projects_355: { num: "100380", name: "缔爱生-女性私密护理组合", Price: "39" }, + Projects_356: { num: "100381", name: "缔爱生-净爽女性私密护理组合", Price: "398" }, + Projects_357: { num: "100382", name: "缔爱生-基础套", Price: "198" }, + Projects_358: { num: "100383", name: "平衡仓疗法", Price: "331" }, + Projects_359: { num: "100384", name: "百草灸疗", Price: "580" }, + Projects_360: { num: "100385", name: "磁量子", Price: "398" }, + Projects_361: { num: "100386", name: "华香龙", Price: "398" }, + Projects_362: { num: "100387", name: "巨邦套盒-浪漫蜜月", Price: "480" }, + Projects_363: { num: "100388", name: "巨邦套盒-倾城之恋", Price: "980" }, + Projects_364: { num: "100389", name: "路乐", Price: "398" }, + Projects_365: { num: "100390", name: "添润SM", Price: "3800" }, + Projects_366: { num: "100391", name: "手部护理", Price: "168" }, + Projects_367: { num: "100392", name: "嘉耀健康", Price: "199000" }, + Projects_368: { num: "100393", name: "肝胆排毒", Price: "3999" }, + Projects_369: { num: "100394", name: "温骨浴", Price: "29.8" }, + Projects_370: { num: "100395", name: "全身排毒", Price: "0" }, + Projects_371: { num: "100396", name: "菲洛", Price: "380" }, + Projects_372: { num: "100397", name: "玉柱活体", Price: "598" }, + Projects_373: { num: "100398", name: "筋雕体态抗衰", Price: "598" }, + Projects_374: { num: "100399", name: "4D系统美胸", Price: "598" }, + Projects_375: { num: "100400", name: "4D淋巴排毒", Price: "598" }, + Projects_376: { num: "100401", name: "暖阳温宫(卵巢)", Price: "480" }, + Projects_377: { num: "100402", name: "肝胆排毒", Price: "480" }, + Projects_378: { num: "100403", name: "固本培元", Price: "480" }, + Projects_379: { num: "100404", name: "深V美胸", Price: "480" }, + Projects_380: { num: "100405", name: "药浴泡澡", Price: "380" }, + Projects_381: { num: "100406", name: "圣门盆腔护理", Price: "980" }, + Projects_382: { num: "100407", name: "圣门体验小套", Price: "698" }, + Projects_383: { num: "100408", name: "瑶浴泡澡", Price: "398" }, + Projects_384: { num: "100409", name: "养肤浴", Price: "398" }, + Projects_385: { num: "100410", name: "洗眼球", Price: "0" }, + Projects_386: { num: "100411", name: "溶脂(RET)", Price: "598" }, + Projects_387: { num: "100412", name: "圣莉斯歌肩颈", Price: "0" }, + Projects_388: { num: "100413", name: "克丽丝胸", Price: "598" }, + Projects_389: { num: "100414", name: "芳香开背", Price: "398" }, + Projects_390: { num: "100415", name: "全身SPA", Price: "880" }, + Projects_391: { num: "100416", name: "局部塑形(598)", Price: "598" }, + Projects_392: { num: "100417", name: "SM卡(299)", Price: "299" }, + Projects_393: { num: "100418", name: "头疗", Price: "598" }, + Projects_394: { num: "100419", name: "基础淋巴", Price: "598" }, + Projects_395: { num: "100420", name: "暖膝通关(肝胆排毒)", Price: "598" }, + Projects_396: { num: "100421", name: "净灵淋巴", Price: "398" }, + Projects_397: { num: "100422", name: "基础肾", Price: "0" }, + Projects_398: { num: "100423", name: "胸博士", Price: "398" }, + Projects_399: { num: "100424", name: "1水疗", Price: "398" }, + Projects_400: { num: "100425", name: "头疗", Price: "498" }, + Projects_401: { num: "100426", name: "葆蒂兰火龙灸", Price: "0" }, + Projects_402: { num: "100427", name: "全身淋巴疏通", Price: "0" }, + Projects_403: { num: "100428", name: "背部经络疏通", Price: "0" }, + Projects_404: { num: "100429", name: "臻极仙丹", Price: "398" }, + Projects_405: { num: "100430", name: "胸部腺体舒通", Price: "0" }, + Projects_406: { num: "100431", name: "围美辣妈", Price: "380" }, + Projects_407: { num: "100432", name: "肠疗排毒", Price: "1980" }, + Projects_408: { num: "100433", name: "全身抛光", Price: "880" }, + Projects_409: { num: "100434", name: "苍南颈部", Price: "0" }, + Projects_410: { num: "100435", name: "圣缇娜盆腔修复套盒", Price: "3325" }, + Projects_411: { num: "100436", name: "全身SPA", Price: "368" }, + Projects_412: { num: "100437", name: "清空头疗", Price: "198" }, + Projects_413: { num: "100438", name: "沉香头疗", Price: "238" }, + Projects_414: { num: "100439", name: "温骨头疗", Price: "580" }, + Projects_415: { num: "100440", name: "睡药疗法", Price: "980" }, + Projects_416: { num: "100441", name: "瑶浴养生头疗套餐", Price: "726" }, + Projects_417: { num: "100442", name: "腹部光子能量理疗", Price: "298" }, + Projects_418: { num: "100443", name: "小芭套", Price: "350" }, + Projects_419: { num: "100444", name: "小蛮腰", Price: "680" }, + Projects_420: { num: "100445", name: "圣缇娜-蜜桃臀", Price: "880" }, + Projects_421: { num: "100446", name: "百草火龙灸", Price: "680" }, + Projects_422: { num: "100447", name: "痛症疗法", Price: "1980" }, + Projects_423: { num: "100448", name: "科普琳胸韵通", Price: "2380" }, + Projects_424: { num: "100449", name: "科普琳胸赋能", Price: "2980" }, + Projects_425: { num: "100450", name: "科普琳胸部精华液", Price: "2580" }, + Projects_426: { num: "100451", name: "圣莉斯歌背", Price: "0" }, + Projects_427: { num: "100452", name: "中医能量调理", Price: "128" }, + Projects_428: { num: "100453", name: "中药泡澡", Price: "120" }, + Projects_429: { num: "100454", name: "秘密花园", Price: "184" }, + Projects_430: { num: "100455", name: "5D", Price: "120" }, + Projects_431: { num: "100456", name: "蒸缸", Price: "198" }, + Projects_432: { num: "100457", name: "排毒", Price: "0" }, + Projects_433: { num: "100458", name: "姜艾灸", Price: "238" }, + Projects_434: { num: "100459", name: "天使瑜伽", Price: "0" }, + Projects_435: { num: "100460", name: "姜丹", Price: "0" }, + Projects_436: { num: "100461", name: "基础淋巴", Price: "0" }, + Projects_437: { num: "100462", name: "体质药浴", Price: "298" }, + Projects_438: { num: "100463", name: "减肥", Price: "6800" }, + Projects_439: { num: "100464", name: "手膜", Price: "1980" }, + Projects_440: { num: "100465", name: "舒活特全身经络", Price: "8800" }, + Projects_441: { num: "100466", name: "背部芳香SPA", Price: "580" }, + Projects_442: { num: "100467", name: "经络疏通", Price: "98" }, + Projects_443: { num: "100468", name: "手膜", Price: "0" }, + Projects_444: { num: "100469", name: "排酸", Price: "0" }, + Projects_445: { num: "100470", name: "私密", Price: "0" }, + Projects_446: { num: "100471", name: "体雕", Price: "0" }, + Projects_447: { num: "100472", name: "海底轮", Price: "0" }, + Projects_448: { num: "100473", name: "唤醒青春", Price: "480" }, + Projects_449: { num: "100474", name: "花庭密钥", Price: "396" }, + Projects_450: { num: "100475", name: "滴水清莲", Price: "1980" }, + Projects_451: { num: "100476", name: "出水芙蓉", Price: "993.3" }, + Projects_452: { num: "100477", name: "雨中芭蕾", Price: "398" }, + Projects_453: { num: "100478", name: "美雕桃花源", Price: "2326.6" }, + Projects_454: { num: "100479", name: "蜜桃少女肌", Price: "2933.3" }, + Projects_455: { num: "100480", name: "候鸟归巢", Price: "18880" }, + Projects_456: { num: "100481", name: "蜜汁女人", Price: "23800" }, + Projects_457: { num: "100482", name: "倾城时光", Price: "29800" }, + Projects_458: { num: "100483", name: "脂肪炸弹", Price: "88" }, + Projects_459: { num: "100484", name: "小火腿", Price: "298" }, + Projects_460: { num: "100485", name: "天鹅颈", Price: "398" }, + Projects_461: { num: "100486", name: "气质肩", Price: "418" }, + Projects_462: { num: "100487", name: "小翘臀", Price: "1100" }, + Projects_463: { num: "100488", name: "造", Price: "0" }, + Projects_464: { num: "100489", name: "尚赫塑形", Price: "298" }, + Projects_465: { num: "100490", name: "循环代谢spa", Price: "198" }, + Projects_466: { num: "100491", name: "宫廷御颜术", Price: "0" }, + Projects_467: { num: "100492", name: "灵性花园性感造型", Price: "0" }, + Projects_468: { num: "100493", name: "性奋小蘑菇", Price: "0" }, + Projects_469: { num: "100494", name: "维蜜性龄测试", Price: "0" }, + Projects_470: { num: "100495", name: "凋零吐纳法游戏", Price: "0" }, + Projects_471: { num: "100496", name: "春暖花开", Price: "0" }, + Projects_472: { num: "100497", name: "康能唤体回春1号", Price: "2980" }, + Projects_473: { num: "100498", name: "康能唤体回春2号", Price: "4980" }, + Projects_474: { num: "100499", name: "康能唤体回春3号", Price: "39800" }, + Projects_475: { num: "100500", name: "焕体灵炙", Price: "1980" }, + Projects_476: { num: "100501", name: "温阳", Price: "398" }, + Projects_477: { num: "100502", name: "化阳", Price: "988" }, + Projects_478: { num: "100503", name: "升阳", Price: "1980" }, + Projects_479: { num: "100504", name: "赫本体雕", Price: "19800" }, + Projects_480: { num: "100505", name: "塑形体验卡", Price: "99" }, + Projects_481: { num: "100506", name: "紫竹排", Price: "3980" }, + Projects_482: { num: "100507", name: "魔罐", Price: "1980" }, + Projects_483: { num: "100508", name: "温养臀", Price: "3980" }, + Projects_484: { num: "100509", name: "刮痧", Price: "0" }, + Projects_485: { num: "100510", name: "皇莎发烧大师", Price: "980" }, + Projects_486: { num: "100511", name: "塑形肚子", Price: "0" }, + Projects_487: { num: "100512", name: "塑形两侧", Price: "0" }, + Projects_488: { num: "100513", name: "塑形背", Price: "0" }, + Projects_489: { num: "100514", name: "塑形胳膊", Price: "0" }, + Projects_490: { num: "100515", name: "塑形腿", Price: "0" }, + Projects_491: { num: "100516", name: "艾灸", Price: "2980" }, + Projects_492: { num: "100517", name: "半年卡", Price: "1990" }, + Projects_493: { num: "100518", name: "年卡", Price: "3980" }, + Projects_494: { num: "100519", name: "胸", Price: "2980" }, + Projects_495: { num: "100520", name: "胸部泥灸", Price: "1980" }, + Projects_496: { num: "100521", name: "泡澡", Price: "980" }, + Projects_497: { num: "100522", name: "胸", Price: "998" }, + Projects_498: { num: "100523", name: "特价攸乐山", Price: "2000" }, + Projects_499: { num: "100524", name: "72变年卡", Price: "0" }, + Projects_500: { num: "100525", name: "脱毛全年", Price: "980" }, + Projects_501: { num: "100526", name: "美背", Price: "580" }, + Projects_502: { num: "100527", name: "唇毛", Price: "0" }, + Projects_503: { num: "100528", name: "脱小腿", Price: "0" }, + Projects_504: { num: "100529", name: "脱大腿", Price: "0" }, + Projects_505: { num: "100530", name: "脱小臂", Price: "0" }, + Projects_506: { num: "100531", name: "脱大臂", Price: "0" }, + Projects_507: { num: "100532", name: "脱手指", Price: "0" }, + Projects_508: { num: "100533", name: "脱腋下", Price: "0" }, + Projects_509: { num: "100534", name: "脱比基尼", Price: "0" }, + Projects_510: { num: "100535", name: "98体验卡", Price: "0" }, + Projects_511: { num: "100536", name: "2021次卡", Price: "0" }, + Projects_512: { num: "100537", name: "2022次卡", Price: "0" }, + Projects_513: { num: "100538", name: "八髎能量回春", Price: "0" }, + Projects_514: { num: "100539", name: "艾灸秒杀", Price: "0" }, + Projects_515: { num: "100540", name: "1980拼团", Price: "0" }, + Projects_516: { num: "100541", name: "1680拼团", Price: "0" }, + Projects_517: { num: "100542", name: "名媛卡", Price: "6800" }, + Projects_518: { num: "100543", name: "沙龙", Price: "1980" }, + Projects_519: { num: "100544", name: "颈部护理", Price: "0" }, + Projects_520: { num: "100545", name: "月光宝盒", Price: "0" }, + Projects_521: { num: "100546", name: "麒麟灸", Price: "0" }, + Projects_522: { num: "100547", name: "肩颈", Price: "0" }, + Projects_523: { num: "100548", name: "臀疗", Price: "0" }, + Projects_524: { num: "100549", name: "1280秒杀", Price: "0" }, + Projects_525: { num: "100550", name: "耳疗", Price: "0" }, + Projects_526: { num: "100551", name: "脾胃", Price: "0" }, + Projects_527: { num: "100552", name: "蒸缸", Price: "0" }, + Projects_528: { num: "100553", name: "泥灸", Price: "0" }, + Projects_529: { num: "100554", name: "维多利亚胸部", Price: "0" }, + Projects_530: { num: "100555", name: "光子床", Price: "0" }, + Projects_531: { num: "100556", name: "砭石", Price: "0" }, + Projects_532: { num: "100557", name: "暖宫", Price: "0" }, + Projects_533: { num: "100558", name: "雷专", Price: "0" }, + Projects_534: { num: "100559", name: "收紧", Price: "0" }, + Projects_535: { num: "100560", name: "水上芭蕾", Price: "0" }, + Projects_536: { num: "100561", name: "老卡", Price: "0" }, + Projects_537: { num: "100562", name: "SPA泡澡", Price: "0" }, + Projects_538: { num: "100563", name: "综合卡", Price: "0" }, + Projects_539: { num: "100564", name: "4D美胸", Price: "0" }, + Projects_540: { num: "100566", name: "私密", Price: "0" }, + Projects_541: { num: "100567", name: "碧波庭", Price: "0" }, + Projects_542: { num: "100568", name: "基础护理", Price: "0" }, + Projects_543: { num: "100569", name: "肠排", Price: "0" }, + Projects_544: { num: "100570", name: "全身SPA", Price: "0" }, + Projects_545: { num: "100571", name: "酵素浴", Price: "0" }, + Projects_546: { num: "100572", name: "姜疗背", Price: "0" }, + Projects_547: { num: "100573", name: "御养舒畅套", Price: "0" }, + Projects_548: { num: "100574", name: "御养舒净套", Price: "0" }, + Projects_549: { num: "100575", name: "御养舒缓套", Price: "0" }, + Projects_550: { num: "100576", name: "御养能量套", Price: "0" }, + Projects_551: { num: "100577", name: "玉颈胶原套", Price: "0" }, + Projects_552: { num: "100578", name: "奢润舒畅套", Price: "0" }, + Projects_553: { num: "100579", name: "芳香舒活套", Price: "0" }, + Projects_554: { num: "100580", name: "蜜养修护套", Price: "0" }, + Projects_555: { num: "100581", name: "清舒畅悦套", Price: "0" }, + Projects_556: { num: "100582", name: "玉肌精华养护套", Price: "0" }, + Projects_557: { num: "100583", name: "佰通堂组合套", Price: "0" }, + Projects_558: { num: "100584", name: "佰通堂艾灸4.0", Price: "0" }, + Projects_559: { num: "100585", name: "佰通堂艾灸5.0", Price: "0" }, + Projects_560: { num: "100586", name: "森林spa", Price: "398" }, + Projects_561: { num: "100587", name: "体龄BOSS", Price: "9800" }, + Projects_562: { num: "100588", name: "体龄BOSS", Price: "980" }, + Projects_563: { num: "100589", name: "胸部体验卡", Price: "399" }, + Projects_564: { num: "100590", name: "娇莺啼 植物抑菌凝胶", Price: "17.8" }, + Projects_565: { num: "100591", name: "娇莺啼 抑菌护理液", Price: "23.8" }, + Projects_566: { num: "100592", name: "阴阳灸", Price: "0" }, + Projects_567: { num: "100593", name: "体龄BOSS", Price: "0" }, + Projects_568: { num: "100594", name: "菁悠排毒", Price: "0" }, + Projects_569: { num: "100595", name: "荷尔蒙", Price: "3980" }, + Projects_570: { num: "100596", name: "药浴水灸", Price: "2980" }, + Projects_571: { num: "100597", name: "蜂姜疗", Price: "3980" }, + Projects_572: { num: "100598", name: "温暖活力套", Price: "398" }, + Projects_573: { num: "100599", name: "性腺疏通", Price: "0" }, + Projects_574: { num: "100600", name: "雨中芭蕾", Price: "880" }, + Projects_575: { num: "100601", name: "私密艾灸", Price: "0" }, + Projects_576: { num: "100602", name: "全身调拨", Price: "398" }, + Projects_577: { num: "100603", name: "肠道水疗手工", Price: "5" }, + Projects_578: { num: "100604", name: "手臂脱毛", Price: "1680" }, + Projects_579: { num: "100605", name: "腋下脱毛", Price: "698" }, + Projects_580: { num: "100606", name: "腿部脱毛", Price: "2680" }, + Projects_581: { num: "100607", name: "出水芙蓉", Price: "198" }, + Projects_582: { num: "100608", name: "体龄磁振仪", Price: "0" }, + Projects_583: { num: "100609", name: "体龄爆脂仪", Price: "0" }, + Projects_584: { num: "100610", name: "体龄筋膜仪", Price: "0" }, + Projects_585: { num: "100611", name: "体龄高频仪", Price: "0" }, + Projects_586: { num: "100612", name: "泥灸", Price: "198" }, + Projects_587: { num: "100613", name: "美带细胞美容", Price: "680" }, + Projects_588: { num: "100614", name: "美带三分钟去眼袋", Price: "680" }, + Projects_589: { num: "100615", name: "美带水氧", Price: "380" }, + Projects_590: { num: "100616", name: "TBS仪器", Price: "598" }, + Projects_591: { num: "100617", name: "暖宫养巢护肾包", Price: "198" }, + Projects_592: { num: "100618", name: "王牌项目", Price: "380" }, + Projects_593: { num: "100619", name: "特色项目", Price: "680" }, + Projects_594: { num: "100620", name: "淋巴", Price: "580" }, + Projects_595: { num: "100621", name: "温控大师", Price: "980" }, + Projects_596: { num: "100622", name: "背轻松", Price: "198" }, + Projects_597: { num: "100623", name: "温控大师", Price: "398" }, + Projects_598: { num: "100624", name: "体雕", Price: "980" }, + Projects_599: { num: "100625", name: "小艾艾灸", Price: "399" }, + Projects_600: { num: "100626", name: "体龄boss仪器单部位体验", Price: "1280" }, + Projects_601: { num: "100627", name: "肩背舒畅", Price: "268" }, + Projects_602: { num: "100628", name: "盆腔闭合", Price: "0" }, + Projects_603: { num: "100629", name: "秘小倩除皱", Price: "0" }, + Projects_604: { num: "100630", name: "无炎排毒元", Price: "0" }, + Projects_605: { num: "100631", name: "爱意如潮", Price: "0" }, + Projects_606: { num: "100632", name: "娇莺啼舒缓", Price: "0" }, + Projects_607: { num: "100633", name: "苗之通肩颈", Price: "0" }, + Projects_608: { num: "100634", name: "苗之清淋巴", Price: "0" }, + Projects_609: { num: "100635", name: "苗之娇胸部", Price: "0" }, + Projects_610: { num: "100636", name: "苗之悦脾胃", Price: "0" }, + Projects_611: { num: "100637", name: "苗之怡盆腔", Price: "0" }, + Projects_612: { num: "100638", name: "苗之源腰部", Price: "0" }, + Projects_613: { num: "100639", name: "苗之润臀部", Price: "0" }, + Projects_614: { num: "100640", name: "苗之灵腿部", Price: "0" }, + Projects_615: { num: "100641", name: "VTS魔术刀", Price: "998" }, + Projects_616: { num: "100642", name: "颐泰圣源花漾", Price: "1988" }, + Projects_617: { num: "100643", name: "颐泰圣源花舒", Price: "2928" }, + Projects_618: { num: "100644", name: "颐泰圣源花滟", Price: "3518" }, + Projects_619: { num: "100645", name: "颐泰圣源花蜜", Price: "3868" }, + Projects_620: { num: "100646", name: "颐泰圣源花箐", Price: "4698" }, + Projects_621: { num: "100647", name: "代言礼:体龄私定打造", Price: "9800" }, + Projects_622: { num: "100648", name: "代言礼:荷尔蒙性线调理", Price: "598" }, + Projects_623: { num: "100649", name: "代言礼:体质排毒调理", Price: "398" }, + Projects_624: { num: "100650", name: "代言礼:高奢抗衰护理", Price: "1980" }, + Projects_625: { num: "100651", name: "体龄单部位", Price: "3800" }, + Projects_626: { num: "100652", name: "无创燃脂", Price: "3800" }, + Projects_627: { num: "100653", name: "520幸福美人腰", Price: "520" }, + Projects_628: { num: "100654", name: "颐泰圣源", Price: "398" }, + Projects_629: { num: "100655", name: "背部净畅", Price: "580" }, + Projects_630: { num: "100656", name: "肩颈舒缓理疗SPA", Price: "398" }, + Projects_631: { num: "100657", name: "超声炮单部位", Price: "3800" }, + Projects_632: { num: "100658", name: "泥灸", Price: "199" }, + Projects_633: { num: "100659", name: "2管理", Price: "999" }, + Projects_634: { num: "100660", name: "抗衰仪器", Price: "999" }, + Projects_635: { num: "100661", name: "小针局部抗衰", Price: "999" }, + Projects_636: { num: "100662", name: "瑶族瑶浴", Price: "0" }, + Projects_637: { num: "100663", name: "温清维系", Price: "0" }, + Projects_638: { num: "100664", name: "温清维系", Price: "0" }, + Projects_639: { num: "100665", name: "七维抗衰单部位", Price: "12800" }, + Projects_640: { num: "100666", name: "善美善康", Price: "398" }, + Projects_641: { num: "100667", name: "巴厘岛SPA(680元)", Price: "680" }, + Projects_642: { num: "100668", name: "盆底肌能见诊评估680元", Price: "680" }, + Projects_643: { num: "100669", name: "背部芳香SPA", Price: "398" }, + Projects_644: { num: "100670", name: "背部净畅护理", Price: "580" }, + Projects_645: { num: "100671", name: "中药泡脚", Price: "0" }, + Projects_646: { num: "100672", name: "霸王卡", Price: "199.9" }, + Projects_647: { num: "100673", name: "铂瑞莱高端全身抗衰", Price: "1980" }, + Projects_648: { num: "100674", name: "元气肩颈SPA", Price: "380" }, + Projects_649: { num: "100675", name: "春德堂", Price: "1280" }, + Projects_650: { num: "100676", name: "春德堂", Price: "1280" }, + Projects_651: { num: "100677", name: "春德堂", Price: "1280" }, + Projects_652: { num: "100678", name: "三叠灸", Price: "288" }, + Projects_653: { num: "100679", name: "鲜草灸", Price: "2980" }, + Projects_654: { num: "100680", name: "植萃润养《寒》套", Price: "2980" }, + Projects_655: { num: "100681", name: "植萃润养《露》套", Price: "2980" }, + Projects_656: { num: "100682", name: "植萃润养《气》套", Price: "2980" }, + Projects_657: { num: "100683", name: "植萃润养《霜》套", Price: "2980" }, + Projects_658: { num: "100704", name: "御森堂泥膜", Price: "380" }, + Projects_659: { num: "100717", name: "特色项目", Price: "0" }, + Projects_660: { num: "100718", name: "闺蜜卡", Price: "1660" }, + Projects_661: { num: "100719", name: "艾灸", Price: "0" }, + Projects_662: { num: "100720", name: "苹果精萃眼部紧致抗衰", Price: "980" }, + Projects_663: { num: "100721", name: "眉眼唇天复疗程护理", Price: "380" }, + Projects_664: { num: "100722", name: "肠道SPA", Price: "680" }, + Projects_665: { num: "100723", name: "淋巴系统调理", Price: "380" }, + Projects_666: { num: "100724", name: "颈脑养护", Price: "880" }, + Projects_667: { num: "100725", name: "圣莉斯歌胸", Price: "880" }, + Projects_668: { num: "100726", name: "圣莉斯歌腹", Price: "880" }, + Projects_669: { num: "100727", name: "圣莉斯歌臀", Price: "880" }, + Projects_670: { num: "100728", name: "私密SPA", Price: "1280" }, + Projects_671: { num: "100729", name: "综合项目", Price: "168" }, + Projects_672: { num: "100731", name: "圣莉斯歌腿", Price: "0" }, + Projects_673: { num: "100732", name: "真美汇体验卡", Price: "5000" }, + Projects_674: { num: "100733", name: "赫菲98卡", Price: "98" }, + Projects_675: { num: "100734", name: "498圣美空间", Price: "0" }, + Projects_676: { num: "100735", name: "脱毛", Price: "88" }, + Projects_677: { num: "100736", name: "空单", Price: "0" }, + Projects_678: { num: "100737", name: "纹眉", Price: "1980" }, + Projects_679: { num: "100738", name: "美丽健康卡", Price: "0" }, + Projects_680: { num: "100739", name: "纹绣", Price: "0" }, + Projects_681: { num: "100740", name: "眉毛", Price: "0" }, + Projects_682: { num: "100741", name: "形体咨询设计", Price: "500" }, + Projects_683: { num: "100742", name: "圣莉斯歌肾", Price: "0" }, + Projects_684: { num: "100743", name: "安康灸疗程", Price: "98" }, + Projects_685: { num: "100744", name: "纹修复", Price: "0" }, + Projects_686: { num: "100745", name: "泥灸", Price: "98" }, + Projects_687: { num: "100746", name: "毛发护理", Price: "19800" }, + Projects_688: { num: "100747", name: "减重", Price: "0" }, + Projects_689: { num: "100748", name: "娇莺啼", Price: "99" }, + Projects_690: { num: "100749", name: "盆骨修复", Price: "99" }, + Projects_691: { num: "100750", name: "特色项目", Price: "0" }, + Projects_692: { num: "100751", name: "佰通堂组合套", Price: "3980" }, + Projects_693: { num: "100752", name: "佰通堂玉肌精华", Price: "2980" }, + Projects_694: { num: "100753", name: "爱香抑菌洗液", Price: "0" }, + Projects_695: { num: "100754", name: "蚕丝蛋白精华", Price: "0" }, + Projects_696: { num: "100755", name: "抑菌精华液", Price: "0" }, + Projects_697: { num: "100756", name: "999卡", Price: "0" }, + Projects_698: { num: "100757", name: "活瓷能量蒸缸", Price: "198" }, + Projects_699: { num: "100758", name: "比基尼小范围", Price: "1280" }, + Projects_700: { num: "100759", name: "比基尼全脱", Price: "2280" }, + Projects_701: { num: "100760", name: "帅薇生发", Price: "0" }, + Projects_702: { num: "100761", name: "帅薇头疗生发", Price: "0" }, + Projects_703: { num: "100762", name: "代言礼:小针医美8选1", Price: "9.99" }, + Projects_704: { num: "100763", name: "免费海外游甄选资格", Price: "4980" }, + Projects_705: { num: "100764", name: "七周年美丽护照", Price: "980" }, + Projects_706: { num: "100765", name: "王牌项目代言基金1000元", Price: "1000" }, + Projects_707: { num: "100766", name: "形体打造抗衰代言金1000元", Price: "1000" }, + Projects_708: { num: "100767", name: "健康美丽沙龙会", Price: "99" }, + Projects_709: { num: "100768", name: "项目体验500元-限生日当天", Price: "500" }, + Projects_710: { num: "100769", name: "项目体验800元-限生日当天", Price: "800" }, + Projects_711: { num: "100770", name: "特色会籍", Price: "40" }, + Projects_712: { num: "100771", name: "项目体验1000元-限生日当天", Price: "1000" }, + Projects_713: { num: "100772", name: "记希莱冻龄素", Price: "1990" }, + Projects_714: { num: "100773", name: "记希莱蓝铜肽", Price: "1990" }, + Projects_715: { num: "100774", name: "胶原蛋白修复", Price: "1990" }, + Projects_716: { num: "100775", name: "腿部纤体塑形护理", Price: "0" }, + Projects_717: { num: "100777", name: "御方泥膜", Price: "380" }, + Projects_718: { num: "100779", name: "头疗", Price: "499" }, + }, + //套餐 + SetMeal: { + SetMeal_1: { + name: "套餐一", + }, + SetMeal_2: { + name: "套餐二", + }, + SetMeal_3: { + name: "套餐三", + }, + SetMeal_4: { + name: "套餐四", + }, + SetMeal_5: { + name: "套餐包TC(BB004)", + Price: "900", + }, + SetMeal_6: { + name: "A套餐(GK035)", + }, + }, + + //产品 + Product: { + Product_1: { + num: "aa100001", + name: "家居搭配护理套", + }, + Product_2: { + num:"aa100002", + name: "启动密码套(滋养套)", + Price:"2980", + }, + Product_3: { + num: "aa100004", + name: "蜜养焕能套", + Price: "8800", + }, + Product_4: { + num: "aa100017", + name: "青春护理套", + Price: "400", + }, + Product_5: { + num: "aa100005", + name: "肌因深层修护原液", + Price: "", + }, + Product_6: { + num: "aa100003", + name: "舒畅修护套", + Price: "", + }, + Product_7: { + num: "aa100014", + name: "抑菌液", + Price: "", + }, + Product_8: { + num: "aa100016", + name: "启动青春套", + Price: "", + }, + Product_9: { + num: "aa100006", + name: "肌因唤醒套", + Price: "", + }, + Product_10: { + num: "aa100009", + name: "粉嫩精华露", + Price: "", + }, + Product_11: { + num: "aa100010", + name: "蜜悦修护6只装", + Price: "", + }, + Product_12: { + num: "aa100019", + name: "舒眠套", + Price: "", + }, + Product_13: { + num: "aa100015", + name: "男士精油", + Price: "", + }, + }, +}; +// 卡类型 +export const CardType = { + //会员卡 + CardTypeList: { + CardType_1: { + name: "会员卡A", + }, + CardType_2: { + name: "会员卡", + }, + CardType_3: { + name: "原价卡", + }, + CardType_4: { + name: "折扣卡", + }, + CardType_5: { + name: "会员卡B", + Price: "1000", + }, + }, +}; +// 优惠券 +export const Coupons = { + //优惠券 + coupon: { + coupon_1: { + name: "定额10元券", + }, + }, +}; diff --git a/tests/fixtures/wasteBookFixture.ts b/tests/fixtures/wasteBookFixture.ts new file mode 100644 index 0000000..83c3c42 --- /dev/null +++ b/tests/fixtures/wasteBookFixture.ts @@ -0,0 +1,14 @@ +import { test as base } from '@playwright/test'; +import { WasteBookBusinessRecordPage } from '@/pages/wastebook'; + +type MyFixture = { wasteBookBusinessRecordPage: WasteBookBusinessRecordPage }; + +export const test = base.extend({ + /** + * @type { WasteBookBusinessRecordPage } + */ + wasteBookBusinessRecordPage: async ({ page }, use) => { + const wasteBookBusinessRecordPage = new WasteBookBusinessRecordPage(page); + await use(wasteBookBusinessRecordPage); + }, +}); diff --git a/tests/imgs/__screenshots__/touch/boss_wastebook.spec.js/流水-营业记录-单据明细-补签-1.png b/tests/imgs/__screenshots__/touch/boss_wastebook.spec.js/流水-营业记录-单据明细-补签-1.png new file mode 100644 index 0000000000000000000000000000000000000000..bdbd0a502e8f4b69e5b11b6c3a6cb6ee71d2dca6 GIT binary patch literal 770 zcmeAS@N?(olHy`uVBq!ia0vp^`9N&U!2~45ULV{Iq!^2X+?^QKos)UVz`(S})5S5Q zV$R!}`Wab)G6y~ummKuHDdv+T#v|5hIiuUiH_PM%OLI)y(Pu4dIk}XKQqL}0q#$-s zN9?qZbKnLEm+nLhHl;bmaTYH>%DvBhS1x<%{p_z5KMMZEtSi}kQ-DX~tiS(*1x~&v z0)&mOwD6cJI^$+;T0DPVLzm$(4#g=!y=m^<&ls+&J)a}6>1IyR&KL`sd1upF1Qd_( z)%{vv(kkH8aWdu8hYt@vR)}@C-hR7p@7~&)nwskB+l(5v4fRGU!pU#Ge%*RA=Udg@ z)2B}dXrye7(pxTY~Gda-Ws*G%J^or@4251D~wl3O`u!CqHqoRP~eI%g{73p7cryT{Nqu*UBG$B!GI z{Qi8nXCu&=T2u4#^4`6F&#Cy}b;*Ad)knvVAOHNhc>C?EufKLJy79JbZ`}X;yJr{g zi_rP>^G}9Jmy1%}{`fvN)9tpGr%d-*eKl+QZL`_B0vs{%@uyY4ojj|}f75Syu#T9o z+T=wVVXMD>j{jf2D&thT$PJ0)#~*h@t=+e0kA+O1hf31Mh;3mLmLI%)Ie7KePrv^j zO!)BU&!6AjA);ra*0R-|F_-FnTJ*ALr%-3h!32hz3+H^knkCxn_WX0D+T=p3xl=B^ zKNBN*HpPgu?!fIFv;O1B1`-R(millUw3P2Z{;}dthRL(fm5VR#_?l6@`LgOFFMqR^ z3E#heui6{8`s$vz_jm8!J$dq^So52kx=Z3dmCf>7-s+TSG&4kN>gCIq*Yh>a+OOLG zIBtD;VPRp(m#5)QD}GMjw(k+&tF`PQ(e}Cv)KXS(N@h8=&6==~SmrnG>>E03e?};C Q0~0lar>mdKI;Vst05iRH^8f$< literal 0 HcmV?d00001 diff --git a/tests/imgs/upload.jpg b/tests/imgs/upload.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3fb3d8e0d4bf5d55f8b0dae82f4016323cf32bbc GIT binary patch literal 6125 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 GXa5VDE@Vmo literal 0 HcmV?d00001 diff --git a/tests/imgs/商品M.jpg b/tests/imgs/商品M.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70c90df93784680cfb5f5da42f67873c5104b3a7 GIT binary patch literal 35888 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 { + await this.page.getByRole('button', { name: /设\s置/ }).click(); + await this.page.getByRole('menuitem', { name: '看板设置' }).click(); + + const $$btn = this.page.locator('.btnBox').getByRole('switch'); + const $$btnCount = await $$btn.count(); + // 遍历每个开关并将其状态设置为“开” + for (let i = 0; i < $$btnCount; i++) { + // 获取当前的开关按钮 + const btn = $$btn.nth(i); + + await expect(async () => { + if ((await btn.innerText()).trim() === '关') { + await btn.click(); + } + await expect(btn).toContainText('开', { timeout: 3_000 }); + }).toPass({ timeout: 30_000 }); + } + // 初始化未到店颜色 + await this.page + .locator('li', { hasText: /^未到店$/ }) + .locator('svg') + .click(); + await this.page.getByLabel('Color:#D0021B').click(); + await this.page.locator('div', { hasText: /^看板设置$/ }).click(); + await this.page.locator('.close > svg').first().click(); + await this.page.reload(); + }; + + /** + * 获取预约时间 + * - 8:00 --> 8:30 + * - 8:30 --> 9:00 + * - 8:50 --> 9:00 + */ + getAppointmentTimesAvailable = (data = new Date()) => { + if (!(data instanceof Date)) { + throw new Error(`传入的${data}不是时间类型`); + } + const currentHour = data.getHours(); + const nextTime = data.getMinutes() > 28 ? ':00' : ':30'; + const hour = String(currentHour + (nextTime === ':00' ? 1 : 0)).padStart(2, '0'); + return `${hour}${nextTime}`; + }; + + /** + * 获取预约状态 + */ + getAppointmentStatusSetting = async () => { + await this.page.getByRole('button', { name: /设\s置/ }).waitFor(); + await this.page.getByText('已过期').waitFor(); + const statusArray = await this.page.locator('.herder_right ul > li').all(); + + for (const status of statusArray) { + // 获取预约状态class name + const statusClass = await status.locator('.status').getAttribute('class'); + let name = statusClass ? statusClass.split(' ')[1] : ''; + // 获取预约状态Color + const color = await status.locator('.status').evaluate(e => { + return window.getComputedStyle(e).backgroundColor; + }); + // 获取预约数量 + const quantity = await status.locator('.number_type').innerText(); + + if (!name || !this.appointmentStatus[name]) { + throw new Error(`没有获取${name}预约状态`); + } + + const appointment = this.appointmentStatus[name]; + appointment.color = color; + appointment.quantity = quantity; + } + + return this.appointmentStatus; + }; + + /** + * 获取顾客预约状态 + * @param {Customer} customer 顾客 + */ + getCustomerAppointmentStatus = async (customer: Customer) => { + const customerLocator = this.page.locator('.a_userInfo', { hasText: customer.username }).first(); + + // 点击顾客信息并获取状态 + await customerLocator.locator('.user_name_info').click(); + const customerStatus = await this.page.locator('.userInfo .state').innerText(); + await this.page.locator('.close > svg > use').first().click(); + + // 获取顾客预约颜色 + const customerColor = await customerLocator.locator('.appointment').evaluate(e => { + return window.getComputedStyle(e).backgroundColor; + }); + + return { customerColor, customerStatus }; + }; + + /** + * 根据员工和时间的中心进行点击预约单元格 + */ + openAppointmentCell = async (name: string, time?: Date, retry = 10) => { + const currentAppointment = time ? this.getAppointmentTimesAvailable(time) : this.getAppointmentTimesAvailable(); + + const $currentTime = this.$$time.filter({ hasText: currentAppointment }); + const currentTimeBoundingBox = await $currentTime.boundingBox(); + + if (name === '未指定') { + await expect(async () => { + if (await this.page.locator('.showNoAppoint_left').isVisible()) { + await this.page.locator('.showNoAppoint_left').click(); + } + + await expect(this.page.locator('.showNoAppoint_right')).toBeVisible({ timeout: 1000 }); + }).toPass(); + } + + const $name = this.$$name.filter({ hasText: name }).last(); + const nameBoundingBox = await $name.boundingBox(); + + if (!currentTimeBoundingBox || !nameBoundingBox) { + throw new Error( + `Could not find bounding boxes: currentTimeBoundingBox=${currentTimeBoundingBox}, nameBoundingBox=${nameBoundingBox}`, + ); + } + + let widthCenter = nameBoundingBox.x + nameBoundingBox.width / 2; // 员工横坐标中心 + let heightCenter = currentTimeBoundingBox.y + currentTimeBoundingBox.height / 2; // 时间纵坐标中心 + const distance = currentTimeBoundingBox.height / 2; // 纵坐标位移一次的距离 + + /** + * 获取元素的中心位置 + */ + const getElementPoint = async (x: number, y: number) => { + return this.page.evaluate(({ x, y }) => document.elementFromPoint(x, y)?.outerHTML, { x, y }); + }; + + for (let attempt = 0; attempt < retry; attempt++) { + const elementPoint = await getElementPoint(widthCenter, heightCenter); + + // 根据是否有 user 单元格进行占用单元格 + if (elementPoint?.includes('user') || elementPoint?.includes('occupy')) { + heightCenter += distance; + } else { + // 点击预约单元格,弹出操作窗口,此处进行了重试 + await expect(async () => { + await this.page.mouse.click(widthCenter, heightCenter, { delay: 2000 }); + await expect(this.page.locator('.popup_content', { hasText: '选择操作' })).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + return; + } + } + + throw new Error(`Unable to click on the appointment cell after ${retry} attempts.`); + }; + + /** + * 打开预约详情 + */ + openAppointmentDetail = async (text: string) => { + await this.page.locator('.a_userInfo', { hasText: text }).first().locator('.user_name_info').click(); + await this.page.getByText('预约详情').waitFor(); + }; + + /** + * 关闭预约详情 + */ + closeAppointmentDetail = async () => { + await this.page.locator('.close > svg > use').first().click(); + }; + + /** + * 取消预约 + */ + cancelAppoint = async () => { + await this.page.locator('.state').waitFor(); + const appointmentState = (await this.page.locator('.state').innerText()).trim(); + + let appointmentTimeStr: string; + let appointmentTime: string; + let hours: any; + let minutes: any; + let date: number | Date; + let currentDate: number | Date; + + switch (appointmentState) { + case '未到店': + appointmentTimeStr = await this.page.locator('.time').innerText(); + appointmentTime = appointmentTimeStr.split('-')[0]; + [hours, minutes] = appointmentTime.split(':').map(Number); + date = new Date(); + date.setHours(hours, minutes, 0, 0); // 设置时、分、秒、毫秒 + + // 获取当前时间 + currentDate = new Date(); + + // 比较时间 + if (date >= currentDate) { + // 当前时间小于或等于指定时间,执行操作 + await this.page.getByRole('button', { name: '取消预约' }).click(); + await this.page.getByRole('button', { name: /确\s认/ }).click(); + await expect(this.page.locator('.ant-message', { hasText: '取消预约成功!' })).toBeVisible(); + } else { + await this.page.locator('.close > svg').first().click(); + } + break; + case '已结算': + await this.page.locator('.close > svg').first().click(); + break; + case '已开单': + await this.page.locator('.close > svg').first().click(); + break; + case '已过期': + await this.page.locator('.close > svg').first().click(); + break; + default: + break; + } + }; + + /** + * 判断元素的中心位置是否在可见视窗内 + */ + elementCenterInViewport = async (element: Locator) => { + const box = await element.boundingBox(); + const viewport = this.page.viewportSize(); + + if (!box) { + return false; + } + + const centerX = box.x + box.width / 2; + const centerY = box.y + box.height / 2; + + return centerX >= 0 && centerX <= viewport!.width && centerY >= 0 && centerY <= viewport!.height; + }; + + /** + * 操作预约 + * @param operation 操作 + * - CANCEL 取消预约 + * - ADD_OCCUPY 新建占用 + * - ADD_APPOINT 新建预约 + * - ADD_REMARK 添加备注 + * - BACK 返回 + * - CANCEL_OCCUPY 取消占用 + */ + operationAppointment = async (operation: AppointmentOperation) => { + // 预约操作窗口 + const $popup = this.page.locator('.popup_content .content'); + // 点击操作 + await expect(async () => { + await $popup.getByText(operation).click(); + await expect($popup.getByText(operation)).not.toBeInViewport({ timeout: 3000 }); + }).toPass(); + }; +} diff --git a/tests/pages/cashierRoomPage.ts b/tests/pages/cashierRoomPage.ts new file mode 100644 index 0000000..3d88224 --- /dev/null +++ b/tests/pages/cashierRoomPage.ts @@ -0,0 +1,39 @@ +import { expect, type Locator, type Page } from '@playwright/test'; +import { type Customer } from '@/utils/customer'; + +export class CashierRoomPage { + page: Page; + $userInfo: Locator; + statusCss: {}; + + /** + * 收银-房态 + */ + constructor(page: Page) { + this.page = page; + this.$userInfo = this.page.locator('.a_userInfo'); + this.statusCss = { + doing: 'doing', + wait: 'wait', + pass: 'pass', + }; + } + + /** + * 检查顾客房间状态 + * @param {import("../utils/customer").Customer} customer + * @param {string} expectedStatus + * - 'doing' 服务中 + * - 'wait' 已占用 + * - 'pass' 已结束 + */ + checkStatus = async (customer: Customer, expectedStatus: string) => { + const userInfo = this.$userInfo.filter({ hasText: customer.username }); + const count = await userInfo.count(); + if (count !== 1) { + throw new Error(`顾客${JSON.stringify(customer)}有${count}个预约房间`); + } + + await expect(userInfo).toHaveClass(new RegExp(`${this.statusCss[expectedStatus]}`)); + }; +} diff --git a/tests/pages/components/index.ts b/tests/pages/components/index.ts new file mode 100644 index 0000000..942a702 --- /dev/null +++ b/tests/pages/components/index.ts @@ -0,0 +1,3 @@ +import { NumberInput } from './numberInput'; + +export { NumberInput }; diff --git a/tests/pages/components/numberInput.ts b/tests/pages/components/numberInput.ts new file mode 100644 index 0000000..8c02505 --- /dev/null +++ b/tests/pages/components/numberInput.ts @@ -0,0 +1,86 @@ +import { Locator, Page } from '@playwright/test'; + +export class NumberInput { + private readonly page: Page; + private readonly popupLocator: Locator; + private readonly inputLocator: Locator; + private readonly confirmButtonLocator: Locator; + private readonly delButtonLocator: Locator; + private readonly delAllButtonLocator: Locator; + private readonly pointInputLocator: Locator; + private readonly commonInputLocator: Locator; + + /** + * 数字键盘组件 + * @param page 页面 + */ + constructor(page: Page) { + this.page = page; + this.popupLocator = this.page.locator('div.popup_content'); + this.commonInputLocator = this.popupLocator.getByPlaceholder(''); + this.inputLocator = this.popupLocator.getByPlaceholder('请输入内容'); + this.pointInputLocator = this.popupLocator.getByPlaceholder('请输入积分'); + this.confirmButtonLocator = this.popupLocator.locator('button.sure'); + this.delButtonLocator = this.popupLocator.locator('button.del'); + this.delAllButtonLocator = this.popupLocator.locator('button.delAll'); + } + + /** + * 设置通用值 + */ + async setCommonValue(value: number): Promise { + await this.commonInputLocator.fill(value.toString()); + } + + /** + * 设置值 + * @param value + */ + async setValue(value: number): Promise { + await this.inputLocator.fill(value.toString()); + } + + /** + * 设置文本 + */ + async setString(value: string): Promise { + await this.inputLocator.fill(value); + } + + /** + * 设置积分值 + * @param value + */ + async setPointValue(value: number): Promise { + await this.pointInputLocator.fill(value.toString()); + } + + /** + * 点击数字键盘 + * @param value + */ + async setInputValue(value: number): Promise { + await this.page.getByRole('button', { name: value.toString() }).click(); + } + + /** + * 点击确认按钮 + */ + async confirmValue(): Promise { + await this.confirmButtonLocator.click(); + } + + /** + * 点击删除按钮 + */ + async delValue(): Promise { + await this.delButtonLocator.click(); + } + + /** + * 点击清空按钮 + */ + async delAllValue(): Promise { + await this.delAllButtonLocator.click(); + } +} diff --git a/tests/pages/customer/customerAnalysisPage.ts b/tests/pages/customer/customerAnalysisPage.ts new file mode 100644 index 0000000..4447035 --- /dev/null +++ b/tests/pages/customer/customerAnalysisPage.ts @@ -0,0 +1,36 @@ +import { Page } from 'playwright'; + +export class CustomerAnalysisPage { + page: Page; + subPages: { name: string; url?: string[] }[]; + + /** + * 顾客分析页面 + * @param {import("@playwright/test").Page} page + */ + constructor(page: Page) { + this.page = page; + this.subPages = [{ name: '项目余量分析' }, { name: '套餐消耗升单分析' }, { name: '顾客项目分析' }]; + } + + /** + * 跳转到子页面 + * @param {string} subPageName + * - 项目余量分析 + * - 套餐消耗升单分析 + * - 顾客项目分析 + */ + gotoSubPage = async (subPageName: string) => { + if (!this.subPages.some(({ name }) => name === subPageName)) { + throw new Error(`${subPageName} is not a valid sub page name`); + } + const $dropDown = this.page + .locator('.top_tab .tab_item', { + hasText: '顾客分析', + }) + .locator('.ant-dropdown-link'); + + await $dropDown.click(); + await this.page.getByRole('menuitem', { name: subPageName }).click(); + }; +} diff --git a/tests/pages/customer/customerDetailsPage.ts b/tests/pages/customer/customerDetailsPage.ts new file mode 100644 index 0000000..ac81ec5 --- /dev/null +++ b/tests/pages/customer/customerDetailsPage.ts @@ -0,0 +1,31 @@ +import { expect, Page } from '@playwright/test'; + +export class CustomerDetailsPage { + page: Page; + subPages: { name: string; url?: string[] }[]; + /** + * 顾客详情页面 + */ + constructor(page: Page) { + this.page = page; + this.subPages = [{ name: '基本资料' }, { name: '流水' }, { name: '动态' }, { name: '日志' }]; + } + + /** + * + * @param subPageName 子页面名称 + * - 基本资料 + * - 流水 + * - 动态 + * - 日志 + */ + gotoSubPage = async (subPageName: string) => { + if (!this.subPages.findIndex(e => e.name === subPageName)) { + throw new Error(`${subPageName} is not in the subPages list`); + } + const $subPage = this.page.getByRole('tab', { name: subPageName }); + await $subPage.click(); + await expect($subPage).toHaveClass(/active/); + await this.page.waitForLoadState(); + }; +} diff --git a/tests/pages/customer/customerPage.ts b/tests/pages/customer/customerPage.ts new file mode 100644 index 0000000..f0a4ac2 --- /dev/null +++ b/tests/pages/customer/customerPage.ts @@ -0,0 +1,379 @@ +import { expect, type Locator, type Page } from '@playwright/test'; +import { HomeNavigation } from '../homeNavigationPage.js'; +import { Customer, employee } from '../../utils/customer'; +import { waitSpecifyApiLoad } from '../../utils/utils.js'; + +type SubPage = { + name: string; + url: string[]; +}; + +/** + * `CustomerPage` 类提供了与应用程序的顾客页面交互的方法。 + * 它包括导航到子页面、搜索顾客、打开和关闭顾客详情、创建新顾客以及将顾客设置为无效的功能。 + */ +export class CustomerPage { + page: Page; + private readonly homeNavigation: HomeNavigation; + private readonly subPages: SubPage[] = [ + { name: '顾客概要', url: ['summary', 'todo'] }, + { name: '顾客分配', url: ['search_new', 'distribution'] }, + { name: '顾客动态', url: ['daily_action'] }, + { name: '顾客分析', url: ['analysis'] }, + { name: '服务日志', url: ['service_log'] }, + ]; + private readonly firstStore = { + firstDepartment: { no: 1, name: '美容部' }, + secondDepartment: { no: 2, name: '医美部' }, + }; + private readonly secondStore = { + firstDepartment: { no: 1, name: '美容部' }, + }; + private readonly source: string[] = [ + '邀客', + '员工带客', + '美团', + '大众点评', + '客带客', + '上门客人', + '百度糯米', + '支付宝', + ]; + $register: Locator; + $tabItem: Locator; + $searchInput: Locator; + $searchConfirm: Locator; + + constructor(page: Page) { + this.page = page; + this.$tabItem = this.page.locator('.top_tab .tab_item'); + this.$register = this.page.locator('.regmeber_warp', { hasText: '创建会员' }); + this.$searchInput = this.page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索'); + this.$searchConfirm = this.page.getByText('搜索', { exact: true }); + this.homeNavigation = new HomeNavigation(this.page); + } + + /** + * 跳转子页面,并且等待页面接口加载完成 + * @param {string} subPageName 顾客模块子页面 + * - 顾客概要 + * - 顾客分配 + * - 顾客动态 + * - 顾客分析 + * - 服务日志 + */ + gotoSubPage = async (subPageName: string) => { + const subPage = this.subPages.find(e => e.name === subPageName); + if (!subPage) { + throw new Error(`子页面 ${subPageName} 不存在`); + } + + const $subPage = this.$tabItem.filter({ hasText: subPageName }); + + await $subPage.waitFor(); + + const classAttribute = await $subPage.getAttribute('class', { timeout: 5000 }); + if (classAttribute && classAttribute.includes('active')) { + return; + } + + await Promise.all([ + expect(async () => { + await $subPage.click(); + await expect($subPage).toHaveClass(/active/); + }).toPass(), + waitSpecifyApiLoad(this.page, subPage.url), + ]); + }; + + /** + * 搜索顾客 + * @param text 姓名(拼音首字)、手机号、档案号搜索 + */ + searchCustomer = async (text: string) => { + const $$customerContent = this.page.locator('.member_list_li .custom_content'); + const $customerInfoCard = $$customerContent.filter({ hasText: text }).first(); + + await this.$searchInput.fill(text); + await this.$searchConfirm.click(); + await $customerInfoCard.waitFor(); + }; + + /** + * 选择搜索顾客 + * @param text 姓名(拼音首字)、手机号、档案号搜索 + */ + selectSearchCustomer = async (text: string) => { + const $$customerContent = this.page.locator('.member_list_li .custom_content'); + const $customerInfoCard = $$customerContent.filter({ hasText: text }).first(); + await $customerInfoCard.click(); + await $customerInfoCard.waitFor({ state: 'detached' }); + }; + + /** + * 打开顾客详情页面 + * @param {string} username + * @param {string} phone + */ + openCustomerDetail = async (username: string, phone: string) => { + const $customer = this.page + .locator('.m-table__fixed-left td') + .filter({ hasText: username }) + .filter({ hasText: phone }) + .first(); + const $username = this.page.getByRole('tabpanel').getByText(username); + const $phone = this.page.getByRole('tabpanel').getByText(phone); + + await Promise.all([ + await $customer.click(), + this.page.waitForResponse(res => res.url().includes('/api/customer') && res.status() === 200), + ]); + await $username.waitFor(); + await $phone.waitFor(); + }; + + /** + * 关闭顾客详情页面 + */ + closeCustomerDetail = async () => { + const closeButton = this.page.locator('.member_info_box .close_icons > svg'); + await closeButton.click(); + await closeButton.waitFor({ state: 'detached' }); + }; + + /** + * 创建顾客 + * @param {Customer} customer 顾客 + */ + createCustomer = async (customer: Customer) => { + const customerPage = '/#/member/member-schame'; + await this.page.goto(customerPage); + await this.selectStore(customer.store); + await this.fillCustomerDetails(customer); + await this.selectDepartment(customer); + if (customer.employees.length !== 0) { + await this.selectEmployee(customer.employees); + } + await this.selectGender(customer.gender); + if (customer.birthday) { + await this.selectBirthday(customer.birthday); + } + await this.selectSource(customer.source); + await this.fillRemark(customer.remark); + await this.confirmCreation(customer.phone); + await this.page.locator('.person_content').waitFor(); + console.log(`username:${customer.username}, phone:${customer.phone}创建成功`); + await this.closeCustomerDetail(); + }; + + /** + * 选择门店 + * @param store 门店 + */ + private readonly selectStore = async (store: number) => { + await this.page.locator('.search_store > div').click(); + await this.page.getByText('部门', { exact: true }).waitFor(); + await this.page.locator('.shopSelect_box .shopSelect_shop_content').click(); + await this.page + .locator('.com_picker .label') + .nth(store - 1) + .click(); + await this.page.getByRole('button', { name: /保\s存/ }).click(); + await this.page.getByRole('button', { name: '新增顾客' }).click(); + }; + + /** + * 填写顾客信息 + * @param customer 顾客 + */ + private readonly fillCustomerDetails = async (customer: Customer) => { + await this.$register.getByPlaceholder('请输入姓名').fill(customer.username); + await this.$register.getByPlaceholder('请输入会员手机号').fill(customer.phone); + await this.$register.getByPlaceholder('请输入会员手机号').click(); + + const checkPhoneLocator = this.$register + .locator('.ant-form-item', { hasText: '手机号' }) + .locator('.ant-form-explain'); + if (await checkPhoneLocator.isVisible()) { + const phoneStr = await checkPhoneLocator.innerText(); + if (phoneStr.includes('非法手机号码') || phoneStr.includes('请输入会员手机号')) { + throw new Error(`手机号码:${customer.phone}不正确`); + } + } + + if (customer.archive) { + await this.$register.getByPlaceholder('请输入12位以内的数字或字母').fill(customer.archive); + } + }; + + /** + * 选择部门 + * @param customer 顾客 + */ + private readonly selectDepartment = async (customer: Customer) => { + await this.$register.locator('#register_departmentNo').getByRole('combobox').click(); + if (customer.store === 1) { + if (customer.department === 1) { + await this.page.getByRole('option', { name: this.firstStore.firstDepartment.name }).click(); + } else if (customer.department === 2) { + await this.page.getByRole('option', { name: this.firstStore.secondDepartment.name }).click(); + } + } else if (customer.store === 2) { + if (customer.department === 1) { + await this.page.getByRole('option', { name: this.secondStore.firstDepartment.name }).click(); + } else { + throw new Error(`部门:${customer.department}不存在`); + } + } else { + throw new Error(`门店:${customer.store}不存在`); + } + }; + + /** + * 选择员工 + * @param employees 员工 + */ + private readonly selectEmployee = async (employees: employee[]) => { + await this.$register.locator('.ant-form-item', { hasText: '员工' }).getByRole('list').click(); + for (const employee of employees) { + await this.page + .getByRole('treeitem', { name: employee.level }) + .getByRole('treeitem', { name: employee.name }) + .click(); + } + await this.page.getByRole('button', { name: '确认分配' }).click(); + }; + + /** + * 选择性别 + * @param gender 性别 + */ + private readonly selectGender = async (gender: number) => { + if (gender === 0) { + await this.$register.locator('label').filter({ hasText: '女性' }).click(); + } else if (gender === 1) { + await this.$register.locator('label').filter({ hasText: '男性' }).click(); + } + }; + + /** + * 选择生日 + * @param {object} birthday 生日 + * @param {number} birthday.year 年份 + * @param {number} birthday.month 月份 + * @param {number} birthday.day 日期 + */ + private readonly selectBirthday = async (birthday: { year: number; month: number; day: number }) => { + if (birthday) { + const { year, month, day } = birthday; + const birthdayLocator = this.$register.locator('.ant-form-item', { hasText: '生日' }); + if (year) { + await birthdayLocator.getByText('年份').click(); + await this.page.getByRole('option', { name: `${year}` }).click(); + await expect(this.page.getByRole('option', { name: `${year}` })).not.toBeVisible(); + } + if (month && day) { + await birthdayLocator.getByText('日期').click(); + await this.page.getByRole('option', { name: new RegExp(`^${month}\\s月$`) }).click(); + await this.page.getByRole('option', { name: new RegExp(`^${day}\\s日$`) }).click(); + await this.page.getByRole('button', { name: /确\s认/ }).click(); + } else { + throw new Error(`month:${month}, day:${day}其中一个为空`); + } + } + }; + + /** + * 选择顾客来源 + * @param {number} source 来源 + */ + private readonly selectSource = async (source: number) => { + await this.$register.getByLabel(this.source[source]).click(); + }; + + /** + * 填写备注 + * @param {string} remark 备注 + */ + private readonly fillRemark = async (remark: string) => { + if (remark) { + await this.$register.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + } + }; + + /** + * 确认创建 + * @param {string} phone 手机号 + */ + private readonly confirmCreation = async (phone: string) => { + const [response] = await Promise.all([ + this.page.waitForResponse( + async res => res.url().includes('/invalid_check') && (await res.json()).code === 'SUCCESS', + ), + this.page.getByRole('button', { name: '确认创建' }).click(), + ]); + const responseBody = await response.json(); + + const phoneStatus = responseBody?.content?.status; + if (!phoneStatus) { + return; + } + await this.page.getByText('系统查询到当前手机号被建档后转为无效客,是否要恢复无效客?').waitFor(); + await this.page.getByRole('button', { name: '重新建档' }).click(); + const popupWindow = this.page.locator('.ant-message'); + await popupWindow.waitFor(); + const popupContent = (await popupWindow.innerText()).trim(); + if (popupContent.includes('该手机号码已经被使用')) { + throw new Error(`手机号码 ${phone} 已被使用,无法创建新顾客`); + } + }; + + /** + * 设置顾客为无效客 + * @param {Customer} customer 顾客 + */ + setInvalidCustomer = async (customer: Customer) => { + await this.page.getByRole('button', { name: '新增顾客' }).waitFor(); + // 根据手机号进行搜索,进入顾客详情页面 + await this.page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await this.page.locator('.ant-input-suffix .search_btn', { hasText: '搜索' }).click(); + await this.page.locator('.custom_content', { hasText: customer.phone }).click(); + await this.page.locator('.m-table__fixed-left').getByText(customer.username).first().click(); + + // 设置无效客 + await this.page.locator('.person_content').waitFor(); + await this.page.locator('.person_content .tag_box .more_icon svg').click(); + await this.page.getByRole('menuitem', { name: '设为无效客' }).click(); + const [response] = await Promise.all([ + this.page.waitForResponse( + response => response.url().includes('/customer') && response.request().method() === 'PATCH', + ), + this.page.getByRole('button', { name: /确\s认/ }).click(), + ]); + + const responseBody = await response.json(); + const code = responseBody?.code; + expect(code).toBe('SUCCESS'); + }; + + /** + * 批量创建顾客 + * @param customerArray + */ + createMoreCustomer = async (customerArray: Customer[]) => { + for (const customer of customerArray) { + await this.createCustomer(customer); + } + }; + + /** + * 批量设置无效客 + * @param customerArray + */ + setMoreInvalidCustomer = async (customerArray: Customer[]) => { + for (const customer of customerArray) { + this.page.reload(); + await this.setInvalidCustomer(customer); + } + }; +} diff --git a/tests/pages/customer/index.ts b/tests/pages/customer/index.ts new file mode 100644 index 0000000..971f3b1 --- /dev/null +++ b/tests/pages/customer/index.ts @@ -0,0 +1,5 @@ +import { CustomerAnalysisPage } from './customerAnalysisPage'; +import { CustomerDetailsPage } from './customerDetailsPage'; +import { CustomerPage } from './customerPage'; + +export { CustomerAnalysisPage, CustomerDetailsPage, CustomerPage }; diff --git a/tests/pages/goalPage.ts b/tests/pages/goalPage.ts new file mode 100644 index 0000000..cf56fe2 --- /dev/null +++ b/tests/pages/goalPage.ts @@ -0,0 +1,33 @@ +import { Page } from '@playwright/test'; +import { waitSpecifyApiLoad } from '@/utils/utils.js'; + +export class GoalPage { + readonly page: Page; + subPages: { name: string; url?: string[] }[]; + + constructor(page: Page) { + this.page = page; + this.subPages = [{ name: '员工目标' }, { name: '精细化管理', url: ['/user_kpi_detail'] }]; + } + + /** + * 跳转到子页面 + * @param subPageName - 员工目标 | 精细化管理 + */ + async gotoSubPage(subPageName: string) { + if (!this.subPages.some(({ name }) => name === subPageName)) { + throw new Error(`${subPageName} is not a valid sub page name`); + } + + const $topTab = this.page.locator('.top_tab .tab_item').filter({ hasText: subPageName }); + + if ((await $topTab.getAttribute('class', { timeout: 500 }))?.includes('active')) { + return; + } + + await Promise.all([ + $topTab.click(), + waitSpecifyApiLoad(this.page, this.subPages.find(({ name }) => name === subPageName)?.url), + ]); + } +} diff --git a/tests/pages/homeNavigationPage.ts b/tests/pages/homeNavigationPage.ts new file mode 100644 index 0000000..f041b7b --- /dev/null +++ b/tests/pages/homeNavigationPage.ts @@ -0,0 +1,63 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +export class HomeNavigation { + page: Page; + $$moduleList: Locator; + subPages: { name: string; url?: string[] }[]; + /** + * @param {Page} page + */ + constructor(page: Page) { + this.page = page; + this.$$moduleList = this.page.locator('.left_box .link_item'); + this.subPages = [ + { name: '收银' }, + { name: '预约' }, + { name: '顾客' }, + { name: '流水' }, + { name: '库存' }, + { name: '目标' }, + { name: '营销' }, + { name: '报表' }, + { name: '设置' }, + { name: '其他' }, + ]; + } + + /** + * 跳转到对应模块 + * @param {string} pageName 模块名称 + * - 收银 + * - 预约 + * - 顾客 + * - 流水 + * - 库存 + * - 目标 + * - 营销 + * - 报表 + * - 设置 + * - 其他 + */ + gotoModule = async (pageName: string) => { + const $active = this.$$moduleList.locator('.active_arrow'); + + if (!this.subPages.some(item => item.name === pageName)) { + throw new Error(`Page ${pageName} does not exist`); + } + + // 模块定位器 + const $module = this.$$moduleList.filter({ hasText: pageName }); + // 模块选中按钮 + const $moduleActive = $module.locator('.active_arrow'); + + // 等待选中按钮出现 + await $active.waitFor(); + + await expect(async () => { + if (!(await $moduleActive.isVisible())) { + await $module.click(); + } + await $moduleActive.waitFor({ timeout: 5000 }); + }).toPass({ timeout: 10_000 }); + }; +} diff --git a/tests/pages/inventory/InventoryManagementPage.ts b/tests/pages/inventory/InventoryManagementPage.ts new file mode 100644 index 0000000..9ff25a6 --- /dev/null +++ b/tests/pages/inventory/InventoryManagementPage.ts @@ -0,0 +1,52 @@ +import { type Page } from 'playwright'; +import { waitSpecifyApiLoad } from '@/utils/utils'; + +export class InventoryManagementPage { + page: Page; + subPages: { name: string; url?: string[] }[]; + /** + * 库存管理页 + * @param page + */ + constructor(page: Page) { + this.page = page; + this.subPages = [ + { name: '库存余量表', url: [] }, + { name: '安全库存差额表', url: [] }, + { name: '寄存余量表', url: [] }, + { name: '过期预警表', url: [] }, + ]; + } + + /** + * 跳转子页面 + * @param subPageName 子页面名称 + * - 库存余量表 + * - 安全库存差额表 + * - 寄存余量表 + * - 过期预警表 + */ + async gotoSubPage(subPageName: string) { + if (!this.subPages.some(item => item.name === subPageName)) { + throw new Error(`暂不支持${subPageName}页面`); + } + const $dropDown = this.page + .locator('.top_tab .tab_item', { + hasText: '库存管理', + }) + .locator('.ant-dropdown-link'); + + await $dropDown.click(); + + const subPage = this.subPages.find(item => item.name === subPageName); + + if (subPage?.url) { + await Promise.all([ + this.page.getByRole('menuitem', { name: subPageName }).click(), + waitSpecifyApiLoad(subPage.url), + ]); + } else { + await this.page.getByRole('menuitem', { name: subPageName }).click(); + } + } +} diff --git a/tests/pages/inventory/index.ts b/tests/pages/inventory/index.ts new file mode 100644 index 0000000..7524196 --- /dev/null +++ b/tests/pages/inventory/index.ts @@ -0,0 +1,4 @@ +import { TransferManagementPage } from './inventoryTransferManagementPage'; +import { InventoryManagementPage } from './InventoryManagementPage'; + +export { TransferManagementPage, InventoryManagementPage }; diff --git a/tests/pages/inventory/inventoryTransferManagementPage.ts b/tests/pages/inventory/inventoryTransferManagementPage.ts new file mode 100644 index 0000000..47ba39c --- /dev/null +++ b/tests/pages/inventory/inventoryTransferManagementPage.ts @@ -0,0 +1,43 @@ +import { Page } from 'playwright'; +import { waitSpecifyApiLoad } from '@/utils/utils'; + +export class TransferManagementPage { + page: Page; + subPages: { name: string; url?: string[] }[]; + /** + * 库存-调货管理页面 + * @param {import("@playwright/test").Page} page + */ + constructor(page: Page) { + this.page = page; + this.subPages = [ + { name: '门店要货', url: ['/stock'] }, + { name: '要货单', url: ['/transfer_stock_bill'] }, + { name: '调货管理', url: ['/transfer_stock_bill'] }, + ]; + } + + gotoSubPage = async (subPageName: string) => { + if (!this.subPages.some(item => item.name === subPageName)) { + throw new Error(`暂不支持${subPageName}页面`); + } + const $dropDown = this.page + .locator('.top_tab .tab_item', { + hasText: '调货管理', + }) + .locator('.ant-dropdown-link'); + + await $dropDown.click(); + + const subPage = this.subPages.find(item => item.name === subPageName); + + if (subPage?.url) { + await Promise.all([ + this.page.getByRole('menuitem', { name: subPageName }).click(), + waitSpecifyApiLoad(subPage.url), + ]); + } else { + await this.page.getByRole('menuitem', { name: subPageName }).click(); + } + }; +} diff --git a/tests/pages/marketing/index.ts b/tests/pages/marketing/index.ts new file mode 100644 index 0000000..926d506 --- /dev/null +++ b/tests/pages/marketing/index.ts @@ -0,0 +1,4 @@ +import { MarketingPage } from './marketingPage'; +import { MarketingInviteGuestsPage } from './marketingInviteGuestsPage'; + +export { MarketingPage, MarketingInviteGuestsPage }; diff --git a/tests/pages/marketing/marketingInviteGuestsPage.ts b/tests/pages/marketing/marketingInviteGuestsPage.ts new file mode 100644 index 0000000..d103706 --- /dev/null +++ b/tests/pages/marketing/marketingInviteGuestsPage.ts @@ -0,0 +1,80 @@ +import { type Locator, type Page } from '@playwright/test'; +import { waitSpecifyApiLoad } from '@/utils/utils.js'; + +export class MarketingInviteGuestsPage { + page: Page; + subPages: { name: string; content?: { name: string; url?: string[] }[] }[]; + $$tabItem: Locator; + + /** + * 邀客管理页面 + * @param page + */ + constructor(page: Page) { + this.page = page; + this.$$tabItem = this.page.locator('.tab_item'); + this.subPages = [ + { + name: '邀客管理', + }, + { + name: '返利方案', + }, + { + name: '数据统计', + }, + { + name: '设置', + content: [ + { name: '礼品设置', url: [] }, + { name: '礼品分配', url: ['/user_present'] }, + { name: '返利升级方案', url: [] }, + { name: '设置', url: [] }, + ], + }, + ]; + } + + /** + * 进入子页面 + * @param subPageName 子页面名称 + * - 邀客管理 + * - 返利方案 + * - 数据统计 + * - 设置 + */ + gotoSubPage = async (subPageName: string) => { + const subPage = this.subPages.find(e => e.name === subPageName); + if (!subPage) { + throw new Error(`${subPageName}不存在`); + } + const $tabItem = this.$$tabItem.filter({ hasText: subPageName }); + await $tabItem.click(); + }; + + /** + * 进入设置子页面 + * @param subPageName 子页面名称 + * - 礼品设置 + * - 礼品分配 + * - 返利升级方案 + * - 设置 + */ + gotoSettingSubPage = async (subPageName: string) => { + const $tabItem = this.$$tabItem.filter({ hasText: subPageName }); + const $tabItemDropDown = $tabItem.locator('.ant-dropdown-link'); + const $dropDownSelected = this.page.getByRole('menuitem', { name: subPageName }); + const settingSubPage = this.subPages.find(e => e.name === '设置')?.content?.find(e => e.name === subPageName); + + if (!settingSubPage) { + throw new Error(`${subPageName}不存在`); + } + + // 打开下拉 + await $tabItemDropDown.click(); + // 选择下拉选择 + await Promise.all([$dropDownSelected.click(), waitSpecifyApiLoad(this.page, settingSubPage.url)]); + // 确认选择成功 + await $tabItemDropDown.filter({ hasText: subPageName }).waitFor(); + }; +} diff --git a/tests/pages/marketing/marketingPage.ts b/tests/pages/marketing/marketingPage.ts new file mode 100644 index 0000000..dab1e89 --- /dev/null +++ b/tests/pages/marketing/marketingPage.ts @@ -0,0 +1,83 @@ +import { type Page } from '@playwright/test'; + +export class MarketingPage { + page: Page; + mallSubPages: string[]; + strategySubPages: string[]; + enterpriseWechatSubPages: string[]; + toolSettingSubPages: string[]; + constructor(page: Page) { + this.page = page; + this.mallSubPages = ['个性状态', '上架好物', '商城订单', '商城设置', '品牌推广']; + this.strategySubPages = ['抢购', '团购', '邀客系统', '会员权益', '积分', '推广收益', '优惠券', '直播系统']; + this.enterpriseWechatSubPages = ['营销获客', '顾客管理', '多渠道触达', '内容营销库', '多维度数据统计']; + this.toolSettingSubPages = ['微信平台']; + } + + /** + * 进入子页面 + * @param moduleName 页面名称 + * - 个性状态 + * - 上架好物 + * - 商城订单 + * - 商城设置 + * - 品牌推广 + */ + gotoMallSubPage = async (moduleName: string) => { + const subReportPage = this.mallSubPages.find(e => e === moduleName); + if (!subReportPage) { + throw new Error(`${moduleName}页面不存在`); + } + await this.page.getByText(subReportPage, { exact: true }).click(); + }; + + /** + * 进入营销策略子页面 + * @param moduleName 页面名称 + * - 抢购 + * - 团购 + * - 邀客系统 + * - 会员权益 + * - 积分 + * - 推广收益 + * - 优惠券 + * - 直播系统 + */ + gotoStrategySubPage = async (moduleName: string) => { + const subReportPage = this.strategySubPages.find(e => e === moduleName); + if (!subReportPage) { + throw new Error(`${moduleName}页面不存在`); + } + await this.page.getByText(subReportPage, { exact: true }).click(); + }; + + /** + * 进入企业微信子页面 + * @param moduleName 页面名称 + * - 营销获客 + * - 顾客管理 + * - 多渠道触达 + * - 内容营销库 + * - 多维度数据统计 + */ + gotoEnterpriseWechatSubPage = async (moduleName: string) => { + const subReportPage = this.enterpriseWechatSubPages.find(e => e === moduleName); + if (!subReportPage) { + throw new Error(`${moduleName}页面不存在`); + } + await this.page.getByText(subReportPage, { exact: true }).click(); + }; + + /** + * 进入工具设置 + * @param moduleName 页面名称 + * - 微信平台 + */ + gotoToolSettingSubPage = async (moduleName: string) => { + const subReportPage = this.toolSettingSubPages.find(e => e === moduleName); + if (!subReportPage) { + throw new Error(`${moduleName}页面不存在`); + } + await this.page.getByText(subReportPage, { exact: true }).click(); + }; +} diff --git a/tests/pages/report/cardBalanceChangeReport.ts b/tests/pages/report/cardBalanceChangeReport.ts new file mode 100644 index 0000000..ccfa13e --- /dev/null +++ b/tests/pages/report/cardBalanceChangeReport.ts @@ -0,0 +1,121 @@ +import { type Page } from '@playwright/test'; +import { reportData } from './reportPage'; + +export class CardBalanceChangeReportPage { + page: Page; + private readonly _reportIndex: reportData[]; + /** + * 储值卡卡金变动表 + */ + constructor(page: Page) { + this.page = page; + this._reportIndex = [ + { + name: '期初', + content: [ + { name: '期初卡金', index: -1 }, + { name: '期初赠金', index: -1 }, + ], + }, + { + name: '消费', + content: [ + { name: '消费卡金', index: -1 }, + { name: '消费赠金', index: -1 }, + ], + }, + { + name: '开充卡', + content: [ + { name: '开充卡金', index: -1 }, + { name: '开充赠金', index: -1 }, + ], + }, + { + name: '调整', + content: [ + { name: '修改卡金', index: -1 }, + { name: '修改赠金', index: -1 }, + ], + }, + { + name: '转移', + content: [ + { name: '转移卡金', index: -1 }, + { name: '转移赠金', index: -1 }, + ], + }, + { + name: '结余', + content: [ + { name: '结余卡金', index: -1 }, + { name: '结余赠金', index: -1 }, + ], + }, + { + name: '变动', + content: [ + { name: '卡金变动', index: -1 }, + { name: '赠金变动', index: -1 }, + ], + }, + ]; + } + + get reportIndex() { + return this._reportIndex; + } + + /** + * 更新报表数据索引 + */ + updateReportDataIndex = async () => { + const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th'); + await thLocator.first().waitFor(); + const headers = await thLocator.allInnerTexts(); + + for (const category of this._reportIndex) { + for (const content of category.content!) { + // 第一列是会员卡名称,故+1 + content.index = + headers.findIndex(headerText => { + return headerText === content.name; + }) + 1; + } + } + }; + + /** + * 获取指定卡的报表数据 + * @param cardName 卡名称 + * @param cardData 卡数据 + */ + getSpecifyCardReportData = async (cardName: string, cardData: reportData[]) => { + const tdLocator = this.page + .locator('.table_sub-box .m-table__body-wrapper tr', { hasText: cardName }) + .first() + .locator('td'); + await tdLocator.first().waitFor(); + const headers = await tdLocator.allInnerTexts(); + for (const category of this._reportIndex) { + const cardCategoryIndex = cardData.findIndex(card => card.name === category.name); + if (cardCategoryIndex === -1) { + // 为空时初始化一个对象 + const obj = { + name: category.name, + content: category.content?.map(item => ({ ...item, value: 0, lastValue: 0 })), + }; + cardData.push(obj); + } + + const cardCategory = cardData.find(card => card.name === category.name); + if (!cardCategory || !cardCategory.content) { + throw new Error(`cardCategory:${JSON.stringify(cardCategory)}`); + } + for (const content of cardCategory.content) { + content.lastValue = content.value; + content.value = Number(headers[content.index!] === '--' ? 0 : headers[content.index!]); + } + } + }; +} diff --git a/tests/pages/report/customerConsumptionAnalysisReport.ts b/tests/pages/report/customerConsumptionAnalysisReport.ts new file mode 100644 index 0000000..fcc8d4e --- /dev/null +++ b/tests/pages/report/customerConsumptionAnalysisReport.ts @@ -0,0 +1,103 @@ +import { Page } from '@playwright/test'; +import { type reportData } from './reportPage'; +import { Customer } from '@/utils/customer'; + +export class CustomerConsumptionAnalysisReportPage { + readonly page: Page; + private _reportData: reportData[]; + /** + * 顾客消费分析表 + */ + constructor(page: Page) { + this.page = page; + this._reportData = [ + { name: '到店次数', value: 0, lastValue: 0, index: -1 }, + { name: '消费次数', value: 0, lastValue: 0, index: -1 }, + { name: '消费金额', value: 0, lastValue: 0, index: -1 }, + { + name: '护理(购买)', + content: [ + { name: '现金', value: 0, lastValue: 0, index: -1 }, + { name: '划卡金', value: 0, lastValue: 0, index: -1 }, + ], + }, + ]; + } + + get reportData() { + return this._reportData; + } + + /** + * 设置报表数据的index + */ + updateReportDataIndex = async () => { + const $firstTr = this.page.locator('.m-table__header-wrapper tr').nth(0).locator('th'); + await $firstTr.first().waitFor(); + + const allFirstTrText = await $firstTr.allInnerTexts(); + + const $secondTr = this.page.locator('.m-table__header-wrapper tr').nth(1).locator('th'); + + const allSecondTrText = await $secondTr.allInnerTexts(); + + for (const category of this._reportData) { + // 找寻一级标题的index + if (typeof category.content === 'undefined') { + category.index = allFirstTrText.findIndex(hedader => hedader === category.name); + } + // 找寻二级标题的index + if (category.content) { + for (const content of category.content) { + content.index = allSecondTrText.findIndex(hedader => hedader === content.name); + } + } + } + }; + + /** + * 获取报表内容 + * @param {Customer} customer 卡名称 + */ + updateReportData = async (customer: Customer) => { + await this.page.locator('.main-table-body_tr').filter({ hasText: customer.phone }).waitFor(); + + const customerDataArray = await this.page + .locator('.main-table-body_tr') + .filter({ hasText: customer.phone }) + .locator('td') + .allInnerTexts(); + + for (const category of this._reportData) { + let value: string; + // 找寻一级标题的index + if (typeof category.content === 'undefined') { + category.lastValue = category.value; + if (category.index) { + value = + customerDataArray[category.index].trim() === '--' + ? '0' + : customerDataArray[category.index].trim(); + category.value = Number(value); + } else { + throw new Error(`找不到${category.name}的index`); + } + } + if (category.content) { + // 找寻二级标题的index + for (const content of category.content) { + content.lastValue = content.value; + if (content.index) { + value = + customerDataArray[content.index].trim() === '--' + ? '0' + : customerDataArray[content.index].trim(); + content.value = Number(value); + } else { + throw new Error(`找不到${content.name}的index`); + } + } + } + } + }; +} diff --git a/tests/pages/report/index.ts b/tests/pages/report/index.ts new file mode 100644 index 0000000..f58201b --- /dev/null +++ b/tests/pages/report/index.ts @@ -0,0 +1,19 @@ +import { ReportPage } from './reportPage'; +import { PerformanceSummaryReportPage } from './performanceSummaryReport'; +import { SpendingSummaryReportPage } from './spendingSummaryReport'; +import { PerformanceDetailReportPage } from './performanceDetailReport'; +import { CardBalanceChangeReportPage } from './cardBalanceChangeReport'; +import { ItemSalesConsumptionAccessReportPage } from './itemSalesConsumptionAccessReport'; +import { SalesCostSummaryReportPage } from './salesCostSummaryReport'; +import { CustomerConsumptionAnalysisReportPage } from './customerConsumptionAnalysisReport'; + +export { + ReportPage, + PerformanceSummaryReportPage, + SpendingSummaryReportPage, + PerformanceDetailReportPage, + CardBalanceChangeReportPage, + ItemSalesConsumptionAccessReportPage, + SalesCostSummaryReportPage, + CustomerConsumptionAnalysisReportPage, +}; diff --git a/tests/pages/report/itemSalesConsumptionAccessReport.ts b/tests/pages/report/itemSalesConsumptionAccessReport.ts new file mode 100644 index 0000000..b8e47d6 --- /dev/null +++ b/tests/pages/report/itemSalesConsumptionAccessReport.ts @@ -0,0 +1,129 @@ +import { type Page } from '@playwright/test'; +import { reportData } from './reportPage'; + +export class ItemSalesConsumptionAccessReportPage { + page: Page; + private readonly _reportDataIndex: reportData[]; + /** + * 项目销耗存表 + */ + constructor(page: Page) { + this.page = page; + this._reportDataIndex = [ + { + name: '期初', + content: [ + { name: '期初金额', value: 0, lastValue: 0, index: -1 }, + { name: '期初数量', value: 0, lastValue: 0, index: -1 }, + { name: '期初赠送数量', value: 0, lastValue: 0, index: -1 }, + ], + }, + { + name: '销售', + content: [ + { name: '销售金额', value: 0, lastValue: 0, index: -1 }, + { name: '销售数量', value: 0, lastValue: 0, index: -1 }, + { name: '赠送数量', value: 0, lastValue: 0, index: -1 }, + { name: '购买人数', value: 0, lastValue: 0, index: -1 }, + { name: '项目复购率', value: 0, lastValue: 0, index: -1 }, + { name: '项目销售占比', value: 0, lastValue: 0, index: -1 }, + ], + }, + { + name: '消耗', + content: [ + { name: '消耗金额', value: 0, lastValue: 0, index: -1 }, + { name: '消耗数量', value: 0, lastValue: 0, index: -1 }, + { name: '消耗赠送数量', value: 0, lastValue: 0, index: -1 }, + { name: '消耗人数', value: 0, lastValue: 0, index: -1 }, + { name: '项目消耗占比', value: 0, lastValue: 0, index: -1 }, + ], + }, + { + name: '调整', + content: [ + { name: '调整金额', value: 0, lastValue: 0, index: -1 }, + { name: '换出数量', value: 0, lastValue: 0, index: -1 }, + { name: '换入数量', value: 0, lastValue: 0, index: -1 }, + { name: '更改数量', value: 0, lastValue: 0, index: -1 }, + { name: '期初导入数量', value: 0, lastValue: 0, index: -1 }, + ], + }, + { + name: '转移', + content: [ + { name: '转入金额', value: 0, lastValue: 0, index: -1 }, + { name: '转入成本', value: 0, lastValue: 0, index: -1 }, + { name: '转入数量', value: 0, lastValue: 0, index: -1 }, + { name: '转出金额', value: 0, lastValue: 0, index: -1 }, + { name: '转出成本', value: 0, lastValue: 0, index: -1 }, + { name: '转出数量', value: 0, lastValue: 0, index: -1 }, + ], + }, + { + name: '结余', + content: [ + { name: '结余金额', value: 0, lastValue: 0, index: -1 }, + { name: '结余数量', value: 0, lastValue: 0, index: -1 }, + { name: '结余赠送数量', value: 0, lastValue: 0, index: -1 }, + { name: '过期结余金额', value: 0, lastValue: 0, index: -1 }, + { name: '过期结余数量', value: 0, lastValue: 0, index: -1 }, + { name: '过期结余赠送数量', value: 0, lastValue: 0, index: -1 }, + ], + }, + ]; + } + + /** + * 更新报表数据索引 + */ + updateReportDataIndex = async () => { + const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th'); + await thLocator.first().waitFor(); + const headers = await thLocator.allInnerTexts(); + + for (const category of this._reportDataIndex) { + for (const content of category.content!) { + // 第一列是会员卡名称,故+1 + content.index = + headers.findIndex(headerText => { + return headerText === content.name; + }) + 1; + } + } + }; + + /** + * 获取项目销售消耗报表数据 + * @param itemName 项目名称 + * @param itemData 项目数据 + */ + getSpecifyItemReportData = async (itemName: string, itemData: reportData[]) => { + const tdLocator = this.page + .locator('.table_sub-box .m-table__body-wrapper tr', { hasText: itemName }) + .first() + .locator('td'); + await tdLocator.first().waitFor(); + const headers = await tdLocator.allInnerTexts(); + for (const category of this._reportDataIndex) { + const cardCategoryIndex = itemData.findIndex(card => card.name === category.name); + if (cardCategoryIndex === -1) { + // 为空时初始化一个对象 + const obj = { + name: category.name, + content: category.content?.map(item => ({ ...item, value: 0, lastValue: 0 })), + }; + itemData.push(obj); + } + + const cardCategory = itemData.find(card => card.name === category.name); + if (!cardCategory || !cardCategory.content) { + throw new Error(`cardCategory:${JSON.stringify(cardCategory)}`); + } + for (const content of cardCategory.content) { + content.lastValue = content.value; + content.value = Number(headers[content.index!] === '--' ? 0 : headers[content.index!]); + } + } + }; +} diff --git a/tests/pages/report/performanceDetailReport.ts b/tests/pages/report/performanceDetailReport.ts new file mode 100644 index 0000000..2869cfe --- /dev/null +++ b/tests/pages/report/performanceDetailReport.ts @@ -0,0 +1,85 @@ +import { type Locator, type Page } from '@playwright/test'; +import { type reportData } from './reportPage'; + +export class PerformanceDetailReportPage { + page: Page; + private readonly _reportData: reportData[]; + private subTables: { name: string; locator: Locator }[]; + /** + * 业绩明细表 + */ + constructor(page: Page) { + this.page = page; + this._reportData = [ + { + name: '项目销售', + content: [ + { name: '数量', value: 0, lastValue: 0, index: -1 }, + { name: '金额', value: 0, lastValue: 0, index: -1 }, + { name: '现金业绩', value: 0, lastValue: 0, index: -1 }, + ], + }, + ]; + this.subTables = [ + { name: '项目销售', locator: this.page.locator('.SERVICE_SALE_table') }, + { name: '项目消耗', locator: this.page.locator('.SERVICE_table') }, + { name: '项目套餐包销售', locator: this.page.locator('.PACKAGE_table') }, + { name: '项目卖品销售', locator: this.page.locator('.GOODS_table') }, + { name: '卡销售', locator: this.page.locator('.CARD_table') }, + ]; + } + + get reportData(): reportData[] { + return this._reportData; + } + + /** + * 更新各个表格的索引 + */ + updateReportDataIndex = async () => { + for (const subReportData of this._reportData) { + const subTable = this.subTables.find(subTable => subTable.name === subReportData.name); + if (!subTable || !subTable.locator) { + throw new Error(`${subTable}`); + } + const currentTableLocator = subTable.locator; + await currentTableLocator.waitFor(); + + const allHeader = await currentTableLocator + .locator('.m-table__header-wrapper tr') + .nth(1) + .locator('th') + .allInnerTexts(); + for (const content of subReportData.content!) { + content.index = allHeader.findIndex(headerText => { + return headerText === content.name; + }); + } + } + }; + + /** + * 获取子表的合计 + */ + updateReportDataForTableTotal = async () => { + for (const subReportData of this._reportData) { + const subTable = this.subTables.find(subTable => subTable.name === subReportData.name); + if (!subTable || !subTable.locator) { + throw new Error(`${subTable}`); + } + const currentTableLocator = subTable.locator; + await currentTableLocator.waitFor(); + const allFooterText = await currentTableLocator + .locator('.table_sub-box .m-table-footer td') + .allInnerTexts(); + for (const content of subReportData.content!) { + // 第一个是合计,故+1 + const currentIndex = content.index! + 1; + content.lastValue = content.value; + content.value = Number( + allFooterText[currentIndex].trim() === '--' ? 0 : allFooterText[currentIndex].trim(), + ); + } + } + }; +} diff --git a/tests/pages/report/performanceSummaryReport.ts b/tests/pages/report/performanceSummaryReport.ts new file mode 100644 index 0000000..627ee5d --- /dev/null +++ b/tests/pages/report/performanceSummaryReport.ts @@ -0,0 +1,143 @@ +import { Page } from '@playwright/test'; +import { type reportData } from './reportPage'; + +export class PerformanceSummaryReportPage { + page: Page; + private readonly _reportData: reportData[]; + /** + * 业绩汇总表 + */ + constructor(page: Page) { + this.page = page; + this._reportData = [ + { + name: '营收明细', + content: [ + { name: '现金类总额', value: 0, lastValue: 0, index: 0 }, + { name: '现金', value: 0, lastValue: 0, index: 0 }, + { name: '划卡', value: 0, lastValue: 0, index: 0 }, + { name: '划赠金', value: 0, lastValue: 0, index: 0 }, + ], + }, + { + name: '开支明细', + content: [{ name: '支出总额', value: 0, lastValue: 0, index: 0 }], + }, + { + name: '现金业绩', + content: [ + { name: '项目合计', value: 0, lastValue: 0, index: 0 }, + { name: '卖品业绩', value: 0, lastValue: 0, index: 0 }, + { name: '开充卡业绩', value: 0, lastValue: 0, index: 0 }, + { name: '开卡业绩', value: 0, lastValue: 0, index: 0 }, + { name: '充值业绩', value: 0, lastValue: 0, index: 0 }, + { name: '总现金业绩', value: 0, lastValue: 0, index: 0 }, + ], + }, + { + name: '划卡业绩', + content: [{ name: '项目合计', value: 0, lastValue: 0, index: 0 }], + }, + { + name: '消耗业绩', + content: [ + { name: '总消耗业绩', value: 0, lastValue: 0, index: 0 }, + { name: '项目总数', value: 0, lastValue: 0, index: 0 }, + ], + }, + { + name: '客流', + content: [ + { name: '客数', value: 0, lastValue: 0, index: 0 }, + { name: '客次', value: 0, lastValue: 0, index: 0 }, + ], + }, + { + name: '套餐销售', + content: [ + { name: '总数量', value: 0, lastValue: 0, index: 0 }, + { name: '普通数量', value: 0, lastValue: 0, index: 0 }, + { name: '总金额', value: 0, lastValue: 0, index: 0 }, + { name: '总业绩', value: 0, lastValue: 0, index: 0 }, + ], + }, + { + name: '卖品销售', + content: [ + { name: '总数量', value: 0, lastValue: 0, index: 0 }, + { name: '普通数量', value: 0, lastValue: 0, index: 0 }, + { name: '总金额', value: 0, lastValue: 0, index: 0 }, + { name: '总业绩', value: 0, lastValue: 0, index: 0 }, + ], + }, + { + name: '其他指标', + content: [ + { name: '客单价', value: 0, lastValue: 0, index: 0 }, + { name: '总耗业绩', value: 0, lastValue: 0, index: 0 }, + ], + }, + ]; + } + + get reportData() { + return this._reportData; + } + + /** + * 更新业绩汇总表数据索引 + */ + updateReportDataIndex = async () => { + const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th'); + await thLocator.first().waitFor(); + const headers = await thLocator.allInnerTexts(); + + for (const category of this._reportData) { + for (const content of category.content!) { + let index: number; + let occurrence = 0; + index = headers.findIndex(headerText => { + if (headerText === content.name) { + occurrence++; // 每次找到相同的文本,计数加1 + if (category.name === '卖品销售' || category.name === '划卡业绩') { + // 找到第二次出现的 content.name + return occurrence === 2; + } else { + // 找到第一次出现的 content.name + return occurrence === 1; + } + } + return false; + }); + if (index !== -1) { + content.index = index + 1; + } else { + throw new Error(`没有找到${content.name}列`); + } + } + } + }; + + /** + * 拿取业绩汇总表的汇总数据 + */ + updateReportDataFromTotal = async () => { + // 合计行定位器 + const $totalTr = this.page.locator('.m-table-footer .m-table__row'); + await $totalTr.first().waitFor(); + // 拿取当前的数据 + const $$totalTd = $totalTr.locator('td'); + const totalAllText = await $$totalTd.allInnerTexts(); + if (totalAllText.length === 0) { + return new Error(`${$totalTr.toString()}没有找到`); + } + for (const category of this._reportData) { + for (const content of category.content!) { + content.lastValue = content.value; + content.value = Number( + totalAllText[content.index].trim() === '--' ? 0 : totalAllText[content.index].trim(), + ); + } + } + }; +} diff --git a/tests/pages/report/reportPage.ts b/tests/pages/report/reportPage.ts new file mode 100644 index 0000000..6624835 --- /dev/null +++ b/tests/pages/report/reportPage.ts @@ -0,0 +1,181 @@ +import { expect, type Locator, type Page } from '@playwright/test'; +import { waitSpecifyApiLoad } from '@/utils/utils.js'; + +type subReportPage = { + name: string; + url: string[]; +}; + +export type reportData = { + name: string; + value?: number; + lastValue?: number; + index?: number; + content?: reportDataInfo[]; +}; + +export type reportDataInfo = { + name: string; + value: number; + lastValue: number; + index: number; +}; + +export class ReportPage { + page: Page; + private subPages: subReportPage[] = [ + { name: '业绩汇总表', url: ['/21'] }, + { name: '业绩明细表', url: ['/5'] }, + { name: '项目销耗存表', url: ['/22'] }, + { name: '销售消耗汇总表', url: ['/23'] }, + { name: '顾客消费分析表', url: ['/24'] }, + { name: '储值卡卡金变动表', url: ['/6'] }, + { name: '开支汇总表', url: ['/7'] }, + { name: '顾客分诊明细表', url: ['/101'] }, + ]; + private $closeCustomReportPage: Locator; + private $closeRecommendReportPage: Locator; + + /** + * 报表模块 + */ + constructor(page: Page) { + this.page = page; + this.$closeCustomReportPage = this.page + .locator('div') + .filter({ hasText: /^自定义报表$/ }) + .locator('svg'); + this.$closeRecommendReportPage = this.page + .locator('div') + .filter({ hasText: /^推荐报表$/ }) + .locator('svg'); + } + + /** + * @param subPageName 报表名称 + * - 业绩汇总表 + * - 业绩明细表 + * - 项目销耗存表 + * - 销售消耗汇总表 + * - 顾客消费分析表 + * - 储值卡卡金变动表 + * - 开支汇总表 + * - 顾客分诊明细表 + */ + gotoSubPage = async (subPageName: string) => { + const subPage = this.subPages.find(e => e.name === subPageName); + if (!subPage) { + throw new Error(`子页面 ${subPageName} 不存在`); + } + await Promise.all([ + this.page.locator('.m-report_recommend .item').filter({ hasText: subPage.name }).click(), + waitSpecifyApiLoad(this.page, subPage.url), + ]); + await expect(this.page.locator('span').filter({ hasText: '推荐报表' })).toBeVisible(); + }; + + /** + * 关闭自定义表格 + */ + closeCustomReportPage = async () => { + await this.$closeCustomReportPage.click(); + }; + + /** + * 关闭推荐表格 + */ + closeRecommendReportPage = async () => { + await this.$closeRecommendReportPage.click(); + }; + + /** + * 对比报表历史数据 + * @param mainClassName 主类名称 + * @param secondaryClassName 次类名称 + * @param reportData 报表数据 + * @param expected 期望值 + */ + toBeReportDataAsExpected = ( + mainClassName: string, + secondaryClassName: string, + reportData: reportData[], + expected: number, + ) => { + const mainClass = reportData.find(e => e.name === mainClassName); + + if (!mainClass) { + throw new Error('mainClass值不对'); + } + + if ( + secondaryClassName === '' || + (mainClass?.content?.length === 0 && typeof mainClass.content === 'undefined') + ) { + if (mainClass.value === undefined || mainClass.lastValue === undefined) { + throw new Error(`mainClass:${mainClassName} 的值或 lastValue 未定义`); + } + expect.soft(mainClass.value - mainClass.lastValue).toBe(expected); + } else { + if (!mainClass.content) { + throw new Error(`mainClass:${mainClass.content}`); + } + const secondaryClass = mainClass.content.find(e => e.name === secondaryClassName); + + if (!secondaryClass) { + throw new Error(`secondaryClass:${secondaryClassName}`); + } + + expect.soft(secondaryClass.value! - secondaryClass.lastValue!).toBe(expected); + } + }; + + /** + * 打开自定义报表页面 + * @param subReportPageName + * - 业绩汇总表 + * - 业绩明细表 + * - 项目销耗存表 + * - 销售消耗汇总表 + * - 顾客消费分析表 + * - 存储卡卡金变动表 + * - 开支汇总表 + * - 顾客分诊明细表 + */ + openCustomReportPage = async (subReportPageName: string) => { + const subReportPage = this.subPages.find(e => e.name === subReportPageName); + if (!subReportPage) { + throw new Error(`子页面 ${subReportPageName} 不存在`); + } + + await this.page + .locator('.m-report_recommend .item') + .filter({ hasText: `${subReportPageName}` }) + .locator('.opreate') + .click(); + }; + + /** + * 删除自定义报表 + * @param reportName 自定义报表名称 + */ + deleteCustomReportPage = async (reportName: string) => { + await this.page + .locator('.m-report_custorm .item') + .filter({ has: this.page.getByText(reportName) }) + .locator('.arrow') + .click(); // 点击箭头 + await this.page.getByRole('menuitem', { name: '删除' }).click(); // 点击删除 + await this.page.getByRole('button', { name: /确\s认/ }).click(); // 点击确认 + }; + + deleteAllCustomReportPage = async () => { + const $$item = this.page.locator('.m-report_custorm .item'); + const itemsCount = await this.page.locator('.m-report_custorm .item').count(); // 获取所有符合条件的元素 + + for (let i = 0; i < itemsCount; i++) { + await $$item.first().locator('.arrow').click(); // 点击箭头 + await this.page.getByRole('menuitem', { name: '删除' }).click(); // 点击删除 + await this.page.getByRole('button', { name: /确\s认/ }).click(); // 点击确认 + } + }; +} diff --git a/tests/pages/report/salesCostSummaryReport.ts b/tests/pages/report/salesCostSummaryReport.ts new file mode 100644 index 0000000..5cb4d0c --- /dev/null +++ b/tests/pages/report/salesCostSummaryReport.ts @@ -0,0 +1,87 @@ +import { Page } from '@playwright/test'; +import { reportData } from './reportPage'; + +export class SalesCostSummaryReportPage { + page: Page; + _reportData: reportData[]; + /** + * 销售消耗汇总表 + */ + constructor(page: Page) { + this.page = page; + this._reportData = [ + { + name: '护理', + content: [ + { name: '现金业绩', index: -1 }, + { name: '划卡业绩', index: -1 }, + { name: '消耗业绩', index: -1 }, + ], + }, + { + name: '面部', + content: [ + { name: '现金业绩', index: -1 }, + { name: '划卡业绩', index: -1 }, + { name: '消耗业绩', index: -1 }, + ], + }, + { + name: '身体', + content: [ + { name: '现金业绩', index: -1 }, + { name: '划卡业绩', index: -1 }, + { name: '消耗业绩', index: -1 }, + ], + }, + { + name: '组合项目', + content: [ + { name: '现金业绩', index: -1 }, + { name: '划卡业绩', index: -1 }, + { name: '消耗业绩', index: -1 }, + ], + }, + ]; + } + + get reportData() { + return this._reportData; + } + + updateReportDataIndex = async () => { + const thLocator = this.page.locator('.m-table__header-wrapper tr').first().locator('th[colspan="3"]'); + await thLocator.first().waitFor(); + const headers = await thLocator.allInnerTexts(); + + for (let i = 0; i < headers.length; i++) { + const reportDataTmp = this._reportData.find(e => e.name === headers[i]); + if (!reportDataTmp || !reportDataTmp.content) { + throw new Error(`${reportDataTmp}--${reportDataTmp?.content}`); + } + for (let j = 0; j < 3; j++) { + reportDataTmp.content[j].index = i * 3 + j; + } + } + }; + + updateReportData = async (store: number): Promise => { + // 没有总部故-1 + const trLocator = this.page.locator('.main-table-body_tr').nth(store - 1); + await trLocator.first().waitFor(); + const tdLocator = trLocator.locator('td'); + const headers = await tdLocator.allInnerTexts(); + if (headers.length === 0) { + throw new Error('没有获取到数据'); + } + for (const category of this._reportData) { + for (const content of category.content!) { + // 保存上一次的数据 + content.lastValue = content.value; + // 拿取当前的数据,currentIndex第一列为门店,故+1 + const currentIndex = content.index! + 1; + content.value = Number(headers[currentIndex].trim() === '--' ? 0 : headers[currentIndex].trim()); + } + } + }; +} diff --git a/tests/pages/report/spendingSummaryReport.ts b/tests/pages/report/spendingSummaryReport.ts new file mode 100644 index 0000000..8047ea5 --- /dev/null +++ b/tests/pages/report/spendingSummaryReport.ts @@ -0,0 +1,89 @@ +import { Page } from '@playwright/test'; +import { reportData } from './reportPage'; + +export class SpendingSummaryReportPage { + page: Page; + _reportData: reportData[]; + /** + * 开支汇总表 + */ + constructor(page: Page) { + this.page = page; + this._reportData = [ + { + name: '支出汇总', + content: [ + { name: '累计支出', value: 0, lastValue: 0, index: -1 }, + { name: '营业收入支出', value: 0, lastValue: 0, index: -1 }, + { name: '备用金支出', value: 0, lastValue: 0, index: -1 }, + ], + }, + { + name: '日常开支', + content: [{ name: '顾客餐', value: 0, lastValue: 0, index: -1 }], + }, + ]; + } + + get reportData(): reportData[] { + return this._reportData; + } + + /** + * 获取报表数据的列的index + */ + updateReportDataIndex = async (): Promise => { + const thLocator = this.page.locator('.m-table__header-wrapper tr').last().locator('th'); + await thLocator.first().waitFor(); + const headers = await thLocator.allInnerTexts(); + + for (const category of this._reportData) { + for (const content of category.content!) { + // 拿取th.name列的列数 + let index = -1; + let occurrence = 0; + index = headers.findIndex(headerText => { + if (headerText === content.name) { + occurrence++; // 每次找到相同的文本,计数加1 + if (category.name === '卖品销售') { + // 找到第二次出现的 content.name + return occurrence === 2; + } else { + // 找到第一次出现的 content.name + return occurrence === 1; + } + } + return false; + }); + if (index !== -1) { + content.index = index + 1; + console.log(`${content.name} 列是第 ${index + 1} 列`); + } else { + throw new Error(`没有找到${content.name}列`); + } + } + } + }; + + /** + * + * @param {1|2} store + */ + updateReportData = async (store: number): Promise => { + const trLocator = this.page.locator('.main-table-body_tr').nth(store); + await trLocator.first().waitFor(); + for (const category of this._reportData) { + for (const content of category.content!) { + // 保存上一次的数据 + content.lastValue = content.value; + // 拿取当前的数据 + const tdLocator = trLocator.locator('td'); + const headers = await tdLocator.allInnerTexts(); + if (headers.length === 0) { + throw new Error('没有获取到数据'); + } + content.value = Number(headers[content.index!].trim() === '--' ? 0 : headers[content.index!].trim()); + } + } + }; +} diff --git a/tests/pages/tablePage.ts b/tests/pages/tablePage.ts new file mode 100644 index 0000000..768b34a --- /dev/null +++ b/tests/pages/tablePage.ts @@ -0,0 +1,208 @@ +import { type Locator, type Page } from 'playwright'; + +export class TablePage { + page: Page; + private readonly _$headerTable: Locator; + private readonly _$bodyTable: Locator; + private readonly _$fixedLeftTable: Locator; + private readonly _$fixedRightTable: Locator; + private readonly _$$bodyTrTable: Locator; + + /** + * Initializes a new instance of the TablePage class. + * + * @param page - The Playwright Page object used to interact with the table elements on the page. + */ + constructor(page: Page) { + this.page = page; + this._$headerTable = page.locator('.m-table__header-wrapper'); + this._$bodyTable = page.locator('.m-table__body-wrapper'); + this._$fixedLeftTable = page.locator('.m-table__fixed-left'); + this._$fixedRightTable = page.locator('.m-table__fixed-right'); + this._$$bodyTrTable = this._$bodyTable.locator('.main-table-body_tr'); + } + + get fixedRightTable() { + return this._$fixedRightTable; + } + + get fixedLeftTable() { + return this._$fixedLeftTable; + } + + get bodyTable() { + return this._$bodyTable; + } + + get bodyTrTable() { + return this._$$bodyTrTable; + } + + get headerTable() { + return this._$headerTable; + } + + /** + * + * @param text + * @param options + * @returns + */ + getFirstHeaderTableIndex = async (text: string, options?: { repeat: number }) => { + const $tr = this._$headerTable.locator('tr').first(); + const $$th = $tr.locator('th'); + await $$th.first().waitFor(); + const firstHeaderText = await $$th.allInnerTexts(); + + if (options?.repeat) { + return this.findNthIndex(firstHeaderText, text, options.repeat); + } + + return firstHeaderText.findIndex(item => item === text); + }; + + /** + * + * @param text + * @param options + * @returns + */ + getSecondHeaderTableIndex = async (text: string, options?: { repeat: number }) => { + const $tr = this._$headerTable.locator('tr').nth(1); + const $$th = $tr.locator('th'); + await $$th.first().waitFor(); + const secondHeaderText = await $$th.allInnerTexts(); + + if (options?.repeat) { + return this.findNthIndex(secondHeaderText, text, options.repeat); + } + + return secondHeaderText.findIndex(item => item === text); + }; + + /** + * + * @param text + * @param options + * @returns + */ + getLeftTableHeaderIndex = async (text: string, options?: { repeat: number }) => { + const $tr = this._$fixedLeftTable.locator('.m-table-fixed-header').locator('thead tr').first(); + const $$th = $tr.locator('th'); + await $$th.first().waitFor(); + const leftTableHeaderText = await $$th.allInnerTexts(); + + if (options?.repeat) { + return this.findNthIndex(leftTableHeaderText, text, options.repeat); + } + + return leftTableHeaderText.findIndex(item => item === text); + }; + + /** + * 异步获取左侧固定列中指定文本的行索引 + * 此函数用于在页面中,根据文本内容查找对应的行索引 + * 它首先定位到左侧固定列的元素,然后遍历元素,等待第一个元素出现, + * 获取所有元素的文本内容,最后返回包含指定文本的行索引 + * 如果未找到匹配的文本,返回-1 + * @param text + * @param index getLeftTableHeaderIndex + * @returns + */ + getLeftTableBodyIndex = async (text: string, index: number, options?: { repeat: number }) => { + const $$tr = this._$fixedLeftTable.locator('.m-table-fixed-body').locator('tbody tr'); + const $$td = $$tr.locator(`td:nth-child(${index + 1})`); + await $$td.first().waitFor(); + const leftTableBodyTdText = await $$td.allInnerTexts(); + + if (options?.repeat) { + return this.findNthIndex(leftTableBodyTdText, text, options.repeat); + } + + return leftTableBodyTdText.findIndex(item => item === text); + }; + + /** + * + * @param text + * @param options + * @returns + */ + getRightTableHeaderIndex = async (text: string, options?: { repeat: number }) => { + const $tr = this._$fixedRightTable.locator('.m-table-fixed-header').locator('thead tr').first(); + const $$th = $tr.locator('th'); + await $$th.first().waitFor(); + const rightTableHeaderText = await $$th.allInnerTexts(); + + if (options?.repeat) { + return this.findNthIndex(rightTableHeaderText, text, options.repeat); + } + + return rightTableHeaderText.findIndex(item => item === text); + }; + + /** + * + * @param text + * @param index + * @param options + * @returns + */ + getRightTableBodyIndex = async (text: string, index: number, options?: { repeat: number }) => { + const $$tr = this._$fixedRightTable.locator('.m-table-fixed-body').locator('tbody tr'); + const $$td = $$tr.locator(`tr:nth-child(${index})`); + await $$td.first().waitFor(); + const rightTableBodyTdText = await $$td.allInnerTexts(); + + if (options?.repeat) { + return this.findNthIndex(rightTableBodyTdText, text, options.repeat); + } + + return rightTableBodyTdText.findIndex(item => item === text); + }; + + /** + * + * @param keywords + * @returns + */ + getBodyTableIndex = async (keywords: string[]) => { + const $$tr = this._$bodyTable.locator('tbody tr'); + await $$tr.first().waitFor(); + + const textArrayCount = await $$tr.count(); + for (let i = 0; i < textArrayCount; i++) { + const temp = await $$tr.nth(i).locator('td').allInnerTexts(); + + const allInnerTexts = keywords.every(text => temp.includes(text)); + + if (allInnerTexts) { + return i; + } + } + return -1; + }; + + /** + * 在数组中查找第n个指定值的索引 + * @param arr 要查找的字符串数组 + * @param value 要查找的值 + * @param n 第几次出现该值 + * @returns 找到的索引,未找到则返回-1 + */ + private findNthIndex(arr: string[], value: string, n: number) { + let count = 0; + let result = -1; + + arr.some((item, index) => { + if (item === value) count++; + if (count === n) { + result = index; + return true; + } + return false; + }); + + return result; + } +} diff --git a/tests/pages/wastebook/index.ts b/tests/pages/wastebook/index.ts new file mode 100644 index 0000000..44bd8dd --- /dev/null +++ b/tests/pages/wastebook/index.ts @@ -0,0 +1,3 @@ +import { WasteBookBusinessRecordPage } from "./wasteBookBusinessRecordPage"; + +export { WasteBookBusinessRecordPage }; \ No newline at end of file diff --git a/tests/pages/wastebook/wasteBookBusinessRecordPage.ts b/tests/pages/wastebook/wasteBookBusinessRecordPage.ts new file mode 100644 index 0000000..bb4d9e4 --- /dev/null +++ b/tests/pages/wastebook/wasteBookBusinessRecordPage.ts @@ -0,0 +1,71 @@ +import { Locator, Page } from 'playwright'; +import { waitSpecifyApiLoad } from '@/utils/utils'; + +export class WasteBookBusinessRecordPage { + page: Page; + $homePage: Locator; + subPages: { name: string; url?: string[] }[]; + /** + * 流水模块-营业记录页面 + * @param {import("@playwright/test").Page} page + */ + constructor(page: Page) { + this.page = page; + this.$homePage = this.page.locator('.top_tab .tab_item', { + hasText: '营业记录', + }); + this.subPages = [ + { name: '营业记录', url: ['/settled_flow'] }, + { name: '业绩流水', url: ['/perf_flow'] }, + { name: '对账流水', url: ['/payment_flow'] }, + ]; + } + + /** + * 进入营业记录子页面 + * @param subPageName 子页面名称 + * - 营业记录 + * - 业绩流水 + * - 对账流水 + */ + gotoSubPage = async (subPageName: string) => { + const subPage = this.subPages.find(item => item.name === subPageName); + if (!subPage) { + throw new Error(`未找到${subPageName}页面`); + } + + const $dropDown = this.$homePage.locator('.ant-dropdown-link'); + await $dropDown.click(); + + const $subPage = this.page.getByRole('menuitem', { name: subPageName }); + await Promise.all([$subPage.click(), waitSpecifyApiLoad(this.page, subPage.url)]); + }; + + /** + * 根据billSet进行撤单操作 + * 配合billSet夹具使用(baseFixture) + */ + revokeOrder = async (billSet: Set) => { + // 撤掉在预约中开的单 + if (billSet.size === 0) { + return; + } + await this.page.getByText('已结算', { exact: true }).waitFor(); + for (const bill of billSet) { + await this.page.getByRole('button').click(); + await this.page.getByPlaceholder('输入流水单号搜索').fill(bill); + await this.page.locator('.search_btn > svg').click(); + const $fixedRightTable = this.page.locator('.m-table__fixed-right'); + await $fixedRightTable.locator('td').nth(1).getByText('撤单').click(); + await this.page.getByPlaceholder('请输入1-100个字符备注内容').click(); + await this.page.getByPlaceholder('请输入1-100个字符备注内容').fill('撤单'); + await this.page.getByRole('button', { name: /确\s认/ }).click(); + try { + await this.page.locator('.ant-message', { hasText: '撤单成功' }).waitFor({ timeout: 3000 }); + } catch (error) { + console.log(`撤单失败,流水单号:${bill}`); + await this.page.getByRole('button', { name: '我知道了' }).click(); + } + } + }; +} diff --git a/tests/setup/boss_auth.setup.ts b/tests/setup/boss_auth.setup.ts new file mode 100644 index 0000000..1af27a2 --- /dev/null +++ b/tests/setup/boss_auth.setup.ts @@ -0,0 +1,70 @@ +import { test as setup } from '@playwright/test'; +import { readIndexedDB } from '@/utils/indexedDBUtils'; +import fs from 'fs'; + +const firstAuthFile = '.auth/admin_first.json'; +const secondAuthFile = '.auth/admin_second.json'; + +const regex = /^https?:\/\/(www\.)?hlk\.meiguanjia\.net\/?(#\s)?$/; + +/** @param personalizedPages 营销-个性化页面*/ +const personalizedPages = '/#/marketing/brand/personalized'; + +class testAccount { + constructor(account, password, path) { + this.account = account; + this.password = password; + this.path = path; + } + + account; + password; + path; +} + +const allAccounts = [ + new testAccount(process.env.boss_account, process.env.boss_password, firstAuthFile), + new testAccount(process.env.boss_account_2, process.env.boss_password_2, secondAuthFile), +]; + +for (let a of allAccounts) { + setup(`租户${a.account}总部管理员登录`, async ({ page, baseURL }) => { + const account = a.account; + const password = a.password; + const savePath = a.path; + + const $phonePassIcon = page + .locator('div', { has: page.getByRole('textbox', { name: '请输入您的手机号码' }) }) + .locator('.pass_svg'); + + await page.goto(baseURL); + await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(account); + await page.getByRole('textbox', { name: '请输入登录密码' }).fill(password); + await page.getByLabel('请同意慧来客隐私政策和用户协议').check(); + await $phonePassIcon.waitFor(); + await page.getByRole('button', { name: /登\s录/ }).click(); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + + if (regex.test(baseURL)) { + await page.goto(personalizedPages); + await page.getByRole('button', { name: '新增模块' }).waitFor(); + const $sideIframe = page.locator('.side iframe').contentFrame(); + const $logo = $sideIframe.locator('.bar_item', { hasText: '我的' }).locator('.logo'); + await $logo.waitFor(); + await page.reload(); + + await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(account); + await page.getByRole('textbox', { name: '请输入登录密码' }).fill(password); + await page.getByLabel('请同意慧来客隐私政策和用户协议').check(); + await $phonePassIcon.waitFor(); + await page.getByRole('button', { name: /登\s录/ }).click(); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + } + const result = await page.evaluate(async readIndexedDBFnString => { + const readIndexedDBFn = new Function('return ' + readIndexedDBFnString)(); + return await readIndexedDBFn(); + }, readIndexedDB.toString()); + fs.writeFileSync('.auth/admin_first_indexeddb.json', JSON.stringify(result)); + await page.context().storageState({ path: savePath }); + }); +} diff --git a/tests/setup/staff_auth.setup.ts b/tests/setup/staff_auth.setup.ts new file mode 100644 index 0000000..cf456c7 --- /dev/null +++ b/tests/setup/staff_auth.setup.ts @@ -0,0 +1,31 @@ +import { test as setup, expect } from "@playwright/test"; +const authFileArray = [".auth/user_1.json", ".auth/user_2.json"]; +const employee = [ + { + phone: process.env.staff1_account, + password: process.env.staff1_password, + }, + { + phone: process.env.staff2_account, + password: process.env.staff2_password, + }, +]; +setup.describe("员工登录", () => { + for (let i = 0; i < 2; i++) { + setup(`门店员工登录_${i}`, async ({ page }) => { + const baseUrl = process.env.BASE_URL; + const $phone = page.getByRole("textbox", { name: "请输入您的手机号码" }); + const $password = page.getByRole("textbox", { name: "请输入登录密码" }); + const $login = page.getByRole("button", { name: /登\s录/ }); + await page.goto(baseUrl); + await $phone.fill(employee[i].phone); + const checkPhone = page.locator(".ant-row", { has: $phone }).locator(".pass_svg"); + await expect(checkPhone).toBeVisible(); + await $password.fill(employee[i].password); + await page.getByLabel("请同意慧来客隐私政策和用户协议").check(); + await $login.click(); + await expect(page.getByRole("button", { name: /开\s单/ })).toBeEnabled(); + await page.context().storageState({ path: authFileArray[i] }); + }); + } +}); diff --git a/tests/touch/boss_appointment.spec.ts b/tests/touch/boss_appointment.spec.ts new file mode 100644 index 0000000..87e3d30 --- /dev/null +++ b/tests/touch/boss_appointment.spec.ts @@ -0,0 +1,638 @@ +// @ts-check +import { faker } from '@faker-js/faker/locale/zh_CN'; +import { test, expect } from '@/fixtures/boss_common.js'; +import path from 'path'; +import fs from 'fs'; +import { staffData } from '@/fixtures/staff.js'; +import { HomeNavigation } from '@/pages/homeNavigationPage.js'; +import { AppointmentPage } from '@/pages/appointmentPage.js'; +import { AppointmentOperation } from '@/pages/appointmentPage.js'; + +test('使用预约单元格', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => { + const setAppointmentTime = appointmentPage.getAppointmentTimesAvailable(); + const customer = createCustomer; + const employee = staffData.firstStore.firstSector.employee_1; + // 当前可预约时间定位器 + const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime }); + // 顾客预约定位器 + const $customerAppointment = page + .locator('.a_userInfo') + .filter({ hasText: customer.phone }) + .filter({ hasText: customer.username }); + + await test.step('进行未指定预约', async () => { + await homeNavigation.gotoModule('预约'); + + // 将时间盒子滚动到当前窗口可见 + await page.locator('.timeLine_Start').scrollIntoViewIfNeeded(); + await $time.scrollIntoViewIfNeeded(); + + // 打开未指定预约单元格 + await appointmentPage.openAppointmentCell('未指定'); + // 新建预约 + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + // 选择顾客A + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + // 确认新建预约 + await page.getByRole('button', { name: '确认新建' }).click(); + // 判断新建预约成功 + await expect($customerAppointment).toBeVisible(); + await appointmentPage.openAppointmentDetail(customer.phone); + await expect(page.getByText('未到店', { exact: true })).toBeVisible(); + await appointmentPage.closeAppointmentDetail(); + }); + + await test.step('取消未指定预约', async () => { + // 打开未指定预约单元格 + await appointmentPage.openAppointmentDetail(customer.phone); + await page.getByRole('button', { name: '取消预约' }).click(); + // 确认取消预约 + await Promise.all([ + page.getByRole('button', { name: /确\s认/ }).click(), + page.waitForResponse(response => response.url().includes('/reservation_num') && response.status() === 200), + ]); + // 判断取消预约成功 + await expect($customerAppointment).not.toBeVisible(); + }); + + await test.step('指定员工进行预约', async () => { + await $time.scrollIntoViewIfNeeded(); + + // 打开预约选择窗口 + await expect(async () => { + await appointmentPage.openAppointmentCell(employee.name); + await expect(page.locator('.popup_content', { hasText: '选择操作' })).toBeVisible({ timeout: 2000 }); + }).toPass(); + + // 新建预约 + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + + // 在预约窗口确认顾客信息 + await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone); + await expect( + page.locator('.content .right .right_ul_li', { + hasText: employee.name, + }), + ).toBeVisible(); + + // 判断新建预约成功 + await page.getByRole('button', { name: '确认新建' }).click(); + await expect($customerAppointment).toBeVisible(); + await appointmentPage.openAppointmentDetail(customer.phone); + await expect(page.getByText('未到店', { exact: true })).toBeVisible(); + await appointmentPage.closeAppointmentDetail(); + }); + + await test.step('取消预约', async () => { + await page.locator('.a_userInfo', { hasText: customer.username }).first().locator('.user_name_info').click(); + await page.getByRole('button', { name: '取消预约' }).click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect($customerAppointment).not.toBeVisible(); + }); +}); + +test('占用预约单元格', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => { + // 获取当前可预约时间 + let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable(); + + // 创建顾客 + const customer = createCustomer; + // 员工王芳 + const employee = staffData.firstStore.firstSector.employee_3; + // 占用单元格备注 + const remark = '占用预约单元格' + faker.string.alpha(2); + + // 当前可预约时间定位器 + const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime }); + // 员工定位器 + const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name }); + // 顾客预约定位器 + const $customerAppointment = page + .locator('.a_userInfo') + .filter({ hasText: customer.phone }) + .filter({ hasText: customer.username }); + + await test.step('占用单元格', async () => { + // 进入预约模块 + await homeNavigation.gotoModule('预约'); + // 窗口滚动到员工、预约时间可见 + await $time.scrollIntoViewIfNeeded(); + await $employee.scrollIntoViewIfNeeded(); + + // 长按目标位置 + await appointmentPage.openAppointmentCell(employee.name); + // 进行占用单元格 + await Promise.all([ + appointmentPage.operationAppointment(AppointmentOperation.ADD_OCCUPY), + page.waitForResponse(response => response.url().includes('/reservation') && response.status() === 200), + ]); + // 占用成功 + await expect(page.locator('.ant-message', { hasText: '占用成功' })).toBeVisible(); + }); + + await test.step('备注占用单元格', async () => { + // 特殊等待,页面内容已变化,但是去操作时,变为上一个状态,只能等待再去操作 + await page.waitForTimeout(2000); + // 打开占用操作窗口 + await page.locator('.a_userInfo .occupy', { hasText: '占用' }).last().click(); + // 进入添加备注 + await appointmentPage.operationAppointment(AppointmentOperation.ADD_REMARK); + // 进行添加备注,确认添加备注成功 + await page.getByPlaceholder('请输入备注').fill(remark); + await Promise.all([ + page + .locator('.popup_content') + .getByRole('button', { name: /确\s认/ }) + .click(), + page.waitForResponse(response => response.url().includes('/reservation_num') && response.status() === 200), + ]); + + // 判断备注后的单元格存在 + await expect(page.locator('.a_userInfo .occupy', { hasText: remark })).toBeVisible(); + }); + + await test.step('取消占用单元格', async () => { + // 打开占用操作窗口 + await page.locator('.a_userInfo .occupy', { hasText: remark }).click(); + // 取消占用 + await appointmentPage.operationAppointment(AppointmentOperation.CANCEL_OCCUPY); + await expect(async () => { + const confirm = page.locator('.popup_content').getByRole('button', { name: /确\s认/ }); + await confirm.click(); + await expect(confirm).not.toBeVisible(); + }).toPass(); + // 判断取消占用成功 + await expect(page.locator('.ant-message', { hasText: '取消占用成功' })).toBeVisible(); + await expect(page.locator('.a_userInfo .occupy', { hasText: remark })).not.toBeVisible(); + }); + + await test.step('取消占用后,能够进行预约', async () => { + // 重新选择员工王芳,进行预约单元格 + await $employee.scrollIntoViewIfNeeded(); + await $time.scrollIntoViewIfNeeded(); + + // 打开预约单元格 + await appointmentPage.openAppointmentCell(employee.name); + + // 选择顾客去创建预约 + await expect(async () => { + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + await page.getByText('选择会员').waitFor({ timeout: 2000 }); + }).toPass(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + + // 确认进入新建预约页面 + await expect( + page.locator('.content .right .right_ul_li', { + hasText: employee.name, + }), + ).toBeVisible(); + await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约'); + await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone); + + // 新建后,确认新建成功 + await page.getByRole('button', { name: '确认新建' }).click(); + await expect($customerAppointment).toBeVisible(); + }); +}); + +test.describe('预约状态', () => { + test('预约-挂单-结算', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage, billSet }) => { + // 员工4 + const employee = staffData.firstStore.firstSector.employee_4; + // 使用的项目 + const project = { no: '100018', name: '苹果精萃护理', shortName: '精萃护理', price: 980 }; + // 获取当前可预约时间 + const setAppointmentTime = appointmentPage.getAppointmentTimesAvailable(); + // 当前可预约时间定位器 + const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime }); + // 员工定位器 + const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name }); + // 创建顾客 + const customer = createCustomer; + + // 获取设置的预约状态 + let appointmentStatusSetting; + await test.step('获取预约状态', async () => { + await homeNavigation.gotoModule('预约'); + appointmentStatusSetting = await appointmentPage.getAppointmentStatusSetting(); + }); + + await test.step('进行创建预约', async () => { + await $time.scrollIntoViewIfNeeded(); + await $employee.scrollIntoViewIfNeeded(); + // 打开预约单元格,进行预约 + await appointmentPage.openAppointmentCell(employee.name, undefined, 10); + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + await expect.soft(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约'); + await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone); + await page.getByRole('button', { name: '确认新建' }).click(); + await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible(); + }); + + await test.step('判断预约,状态为未到店', async () => { + const { customerColor, customerStatus } = await appointmentPage.getCustomerAppointmentStatus(customer); + // 进入未到店状态 + expect.soft(customerColor).toEqual(appointmentStatusSetting.NORMAL.color); + expect(customerStatus).toEqual(appointmentStatusSetting.NORMAL.name); + }); + + await test.step('进行挂单,状态为已开单', async () => { + await page + .locator('.a_userInfo', { hasText: customer.username }) + .first() + .locator('.user_name_info') + .click(); + // 预约进行挂单 + await page.getByRole('button', { name: '到店开单' }).click(); + await page + .locator('.project_list') + .filter({ hasText: project.name }) + .filter({ hasText: project.no }) + .click(); + await page.locator('.hold_bill', { hasText: '挂单' }).click(); + await expect(page.locator('.ant-message', { hasText: '挂单成功' })).toBeVisible(); + + await homeNavigation.gotoModule('预约'); + + // 获取当前预约的状态 + const { customerColor, customerStatus } = await appointmentPage.getCustomerAppointmentStatus(customer); + // 进入未到店状态 + expect.soft(customerColor).toEqual(appointmentStatusSetting.BILL.color); + expect(customerStatus).toEqual(appointmentStatusSetting.BILL.name); + }); + + await test.step('进行结算,状态为已结算', async () => { + await page + .locator('.a_userInfo', { hasText: customer.username }) + .first() + .locator('.user_name_info') + .click(); + await page.getByRole('button', { name: /取\s单/ }).click(); + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + // 取消推送消费提醒和结算签字 + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /结\s算/ }).click(), + ]); + const responseBody = await response.json(); + const billNo = responseBody?.content?.billNo; + expect(billNo).not.toBeNull(); + billSet.add(billNo); + + // 处理结算后的弹窗 + await page.addLocatorHandler(page.getByRole('button', { name: '我知道了' }), async () => { + await page.getByRole('button', { name: '我知道了' }).click(); + await expect(page.getByRole('button', { name: '我知道了' })).not.toBeVisible(); + await page.reload(); + await homeNavigation.gotoModule('预约'); + }); + + // 进入预约模块 + await homeNavigation.gotoModule('预约'); + // 获取当前创建预约的状态 + const { customerColor, customerStatus } = await appointmentPage.getCustomerAppointmentStatus(customer); + // 进入未到店状态 + expect.soft(customerColor).toEqual(appointmentStatusSetting.SETTLED.color); + expect(customerStatus).toEqual(appointmentStatusSetting.SETTLED.name); + }); + }); + + test.skip('预约-过期', async ({ page, createCustomer, appointmentPage, customerPage }) => { + // 获取预约时间 + let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable(); + + // 创建顾客 + const customer = createCustomer; + // 员工赵军 + const employee = staffData.firstStore.firstSector.employee_5; + // 预约时间定位器 + const $time = page.locator('.times_table td').filter({ hasText: setAppointmentTime }); + // 员工定位器 + const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name }); + + await test.step('选择顾客进行预约', async () => { + await $employee.scrollIntoViewIfNeeded(); + await $time.scrollIntoViewIfNeeded(); + await appointmentPage.openAppointmentCell(employee.name); + await expect(async () => { + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + await expect(page.getByText('选择会员')).toBeVisible(); + }).toPass(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约'); + await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone); + await page.getByRole('button', { name: '确认新建' }).click(); + await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible(); + }); + }); +}); + +test.describe.skip('预约设置', () => { + test('看板设置', async ({ page, homeNavigation, createCustomer, appointmentPage, customerPage }) => { + let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable(); + + const customer = createCustomer; + + const employee = staffData.firstStore.firstSector.employee_6; + let switchStatus; // + let nowNormalColor; // 当前模块颜色 + let nowAppointmentColor; //当前预约颜色 + + const customerRemarkLocator = page + .locator('.appointment_content', { hasText: customer.username }) + .locator('.remark_info', { hasText: '测试看板设置' }); + const normal = page.locator('.herder_right ul > li', { hasText: '未到店' }); + const merchantRemarkLocator = page.locator('li').filter({ hasText: '商家备注' }); + const $time = page.locator('.times_table td'); + const $employee = page.locator('.room_table .tr .name_th').filter({ hasText: employee.name }); + + await test.step('创建预约', async () => { + await homeNavigation.gotoModule('预约'); + await $time.filter({ hasText: setAppointmentTime }).scrollIntoViewIfNeeded(); + await $employee.scrollIntoViewIfNeeded(); + await appointmentPage.openAppointmentCell(employee.name); + + await page.locator('.popup_content', { hasText: '选择操作' }).waitFor(); + await expect(async () => { + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + await page.getByText('选择会员').waitFor({ timeout: 2000 }); + }).toPass(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约'); + await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone); + await page.getByRole('button', { name: '确认新建' }).click(); + await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible(); + }); + + await test.step('给顾客预约添加备注', async () => { + await page + .locator('.a_userInfo', { hasText: customer.username }) + .first() + .locator('.user_name_info') + .click(); + await page.locator('.my_remark .write').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试看板设置'); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.locator('.info_center .close').click(); + + await expect(customerRemarkLocator).toBeVisible(); + }); + + await test.step('修改商家备注为关闭、修改未到店的颜色', async () => { + await page.getByRole('button', { name: /设\s置/ }).click(); + await page.getByRole('menuitem', { name: '看板设置' }).click(); + let switchStatus = page.locator('li').filter({ hasText: '商家备注' }).getByRole('switch'); + await expect(switchStatus).toHaveText('开'); + await page.locator('li').filter({ hasText: '商家备注' }).getByRole('switch').click(); + await expect(switchStatus).toHaveText('关'); + + // 修改未到店的颜色 + await page + .locator('li') + .filter({ hasText: /^未到店$/ }) + .locator('div') + .nth(3) + .click(); + // rgb(0, 0, 0) + // D42525 + await page.getByLabel('r', { exact: true }).fill('0'); + await page.getByLabel('g', { exact: true }).fill('0'); + await page.getByLabel('b', { exact: true }).fill('0'); + + await page.getByText('看板信息').click(); + await page.locator('.close > svg > use').first().click(); + // 获取预约状态Color + nowNormalColor = await normal.locator('.status').evaluate(e => { + return window.getComputedStyle(e).backgroundColor; + }); + + // 获取当前创建预约的状态 + nowAppointmentColor = await page + .locator('.a_userInfo', { hasText: customer.username }) + .first() + .locator('.appointment') + .evaluate(e => { + return window.getComputedStyle(e).backgroundColor; + }); + + // 判断商家备注被关闭 + await expect(customerRemarkLocator).not.toBeVisible(); + // 判断未到店颜色被修改 + expect.soft(nowNormalColor).toEqual('rgb(0, 0, 0)'); + // 判断预约颜色被修改 + expect(nowAppointmentColor).toEqual('rgb(0, 0, 0)'); + }); + + await test.step('恢复默认值', async () => { + // 恢复默认 + await page.getByRole('button', { name: /设\s置/ }).click(); + await page.getByRole('menuitem', { name: '看板设置' }).click(); + switchStatus = merchantRemarkLocator.getByRole('switch'); + await expect(switchStatus).toHaveText('关'); + await merchantRemarkLocator.getByRole('switch').click(); + switchStatus = merchantRemarkLocator.getByRole('switch'); + await expect(switchStatus).toHaveText('开'); + await page + .locator('li') + .filter({ hasText: /^未到店$/ }) + .locator('span') + .nth(1) + .click(); + // await page.getByLabel('hex', { exact: true }).fill("D42525"); + await page.getByLabel('r', { exact: true }).fill('212'); + await page.getByLabel('g', { exact: true }).fill('37'); + await page.getByLabel('b', { exact: true }).fill('37'); + await page.getByText('状态颜色').click(); + await page.locator('.m_sliding_bg').click(); + + // 获取预约状态Color + nowNormalColor = await normal.locator('.status').evaluate(e => { + return window.getComputedStyle(e).backgroundColor; + }); + expect(nowNormalColor).toEqual('rgb(212, 37, 37)'); + }); + }); + + test('导出图片', async ({ page, homeNavigation }) => { + await homeNavigation.gotoModule('预约'); + await page.locator('.herder_right').getByRole('button').nth(1).click(); + const downloadPromise = page.waitForEvent('download'); + await page.getByRole('menuitem', { name: '导出图片' }).click(); + const download = await downloadPromise; + + // 获取下载的文件名 + const fileName = download.suggestedFilename(); + + // 指定保存下载文件的路径 + const downloadPath = path.join(__dirname, `@/imgs/${fileName}`); + + // 保存下载的文件 + await download.saveAs(downloadPath); + + // 验证文件是否成功保存 + const fileExists = fs.existsSync(downloadPath); + expect(fileExists).toBeTruthy(); + }); + + test('视图调整', async ({ page, homeNavigation, appointmentPage }) => { + const cellBox = { height: 0, lastHeight: 0, width: 0, lastWidth: 0 }; // 单元格 + const employee = staffData.firstStore.firstSector.employee_1; // 员工张伟 + let setAppointmentTime = appointmentPage.getAppointmentTimesAvailable(); // 预约时间 + + // 获取当前单元格的宽高 + const $time = page.locator('.times_table td', { hasText: setAppointmentTime }); + const $employee = page.locator('.room_table .tr .name_th', { hasText: employee.name }).first(); + await homeNavigation.gotoModule('预约'); + await $time.scrollIntoViewIfNeeded(); + await $employee.scrollIntoViewIfNeeded(); + let timeBox = await $time.boundingBox(); + let employeeBox = await $employee.boundingBox(); + cellBox.lastHeight = timeBox != null ? timeBox.height : 0; + cellBox.lastWidth = employeeBox != null ? employeeBox.width : 0; + + await page.locator('.herder_right').getByRole('button').nth(1).click(); + await page.getByRole('menuitem', { name: '视图调整' }).click(); + // 打开了自定义的宽高 + await expect(page.locator('.set_w_h_box .set_w_h_title', { hasText: '自定义预约表格宽高' })).toBeVisible(); + + const setHeightLocator = page.locator('.set_w_h_content', { hasText: '高度' }).getByRole('slider'); + const setWidthLocator = page.locator('.set_w_h_content', { hasText: '宽度' }).getByRole('slider'); + + // 获取可以移动最大和最小的宽度和高度 + const heightValueMax = Number(await setHeightLocator.getAttribute('aria-valuemax')); + const heightValueMin = Number(await setHeightLocator.getAttribute('aria-valuemin')); + const widthValueMax = Number(await setWidthLocator.getAttribute('aria-valuemax')); + const widthValueMin = Number(await setWidthLocator.getAttribute('aria-valuemin')); + const heightDistance = heightValueMax - heightValueMin; + const widthDistance = widthValueMax - widthValueMin; + + const setHeightBox = await setHeightLocator.boundingBox(); + const setWidthBox = await setWidthLocator.boundingBox(); + if (setHeightBox == null || setWidthBox == null) { + throw new Error('获取设置高度和宽度的定位器失败'); + } + + await test.step('设置高度', async () => { + await $time.scrollIntoViewIfNeeded(); + // 设置单元格高度 + await page.mouse.move(setHeightBox.x + setHeightBox.width / 2, setHeightBox.y + setHeightBox.height / 2); + await page.mouse.down(); + await page.mouse.move( + setHeightBox.x + setHeightBox.width / 2 + heightDistance, + setHeightBox.y + setHeightBox.height / 2, + ); + await page.mouse.down(); + + timeBox = await $time.boundingBox(); + cellBox.height = timeBox != null ? timeBox.height : 0; + expect(cellBox.height - cellBox.lastHeight).toEqual(heightDistance); + }); + + await test.step('设置宽度', async () => { + // 设置单元格宽度 + await page.mouse.move(setWidthBox.x + setWidthBox.width / 2, setWidthBox.y + setWidthBox.height / 2); + await page.mouse.move(setWidthBox.x, setWidthBox.y); + await page.mouse.down(); + await page.mouse.move( + setWidthBox.x + setWidthBox.width / 2 + widthDistance, + setWidthBox.y + setWidthBox.height / 2, + ); + await page.mouse.down(); + + await $employee.scrollIntoViewIfNeeded(); + employeeBox = await $employee.boundingBox(); + cellBox.width = employeeBox?.width ?? 0; + expect(cellBox.width - cellBox.lastWidth).toEqual(widthDistance); + }); + + // 恢复默认视图设置 + await page.locator('.resetting').click(); + }); +}); + +test.afterEach(async ({ homeNavigation, wasteBookBusinessRecordPage, billSet }) => { + // 撤回预约开的单 + await homeNavigation.gotoModule('流水'); + await wasteBookBusinessRecordPage.revokeOrder(billSet); +}); + +// 清除当前时间之后所有的预约和占用 +test.afterAll(async ({ browser, baseURL }) => { + const page = await browser.newPage(); + await page.goto(baseURL ?? ''); + + const homeNavigation = new HomeNavigation(page); + const appointmentPage = new AppointmentPage(page); + + await homeNavigation.gotoModule('预约'); + await expect(page.getByText('张伟')).toBeVisible(); + const $$userInfo = page.locator('.a_userInfo'); + // 获取顾客占用预约单元格信息 + const $$appointmentBox = $$userInfo.locator('.appointment_content'); + + const names = await $$appointmentBox.locator('.name').allInnerTexts(); + const userPhones = await $$appointmentBox.locator('.user_phone').allInnerTexts(); + + const userInfoData = names.map((name, index) => { + return { + name, + phone: userPhones[index], + }; + }); + + // 取消预约占用 + // 获取占用框的数量 + let occupyBoxCount = await $$userInfo.locator('.occupy').count(); + while (occupyBoxCount > 0) { + // 每次都使用 first() 获取第一个占用框 + const $occupyBox = $$userInfo.locator('.occupy').first(); + await $occupyBox.click(); + await page + .locator('.popup_content .class_content') + .getByText(/^取消占用$/) + .click(); + + try { + // 等待请求成功,确认按钮和响应并行处理 + await Promise.all([ + page.waitForResponse(res => res.url().includes('/reservation') && res.status() === 200, { + timeout: 5000, + }), // 添加超时 + page.getByRole('button', { name: /确\s认/ }).click(), + ]); + } catch (error) { + console.error('取消预约时出现错误: ', error); + } + + await page.waitForTimeout(500); + occupyBoxCount = await $$userInfo.locator('.occupy').count(); + } + + // 取消有取消预约按钮的预约 + for (const user of userInfoData) { + const { name } = user; + const $appointmentBox = $$appointmentBox.filter({ has: page.getByText(name) }).locator('.user_name_info'); + const appointCount = await $appointmentBox.count(); + if (appointCount === 1 && (await appointmentPage.elementCenterInViewport($appointmentBox.first()))) { + await $appointmentBox.first().click(); + await appointmentPage.cancelAppoint(); + } + } + + await page.close(); +}); diff --git a/tests/touch/boss_cashier.spec.ts b/tests/touch/boss_cashier.spec.ts new file mode 100644 index 0000000..12dcacb --- /dev/null +++ b/tests/touch/boss_cashier.spec.ts @@ -0,0 +1,1425 @@ +// @ts-check +import { test, expect } from '@/fixtures/boss_common.js'; //使用公共套件 +import { Feature, Employees, ProjectName, CardType, Coupons } from '@/fixtures/userconfig.js'; +import { decodeQR, KeepOnlyNumbers } from '@/utils/utils.js'; +import { faker } from '@faker-js/faker/locale/zh_CN'; +import path from 'path'; +import { AppointmentOperation } from '@/pages/appointmentPage.js'; + +test.describe('挂单', () => { + let c; + let username; + let phone; + test.beforeEach('测试前新建会员', async ({ page, homeNavigation, createCustomer, customerPage }) => { + // 创建顾客 + c = createCustomer; + // 获取姓名、手机号、档案号 + username = c.username; + phone = c.phone; + + // 开卡 + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + await page.getByText('原价卡项目10折,套餐10折,卖品10折卡金:¥5000').click(); + await page.getByRole('button', { name: '去结算' }).click(); + await page.getByText('现金').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await page.locator('use').first().click(); + }); + + test('查看员工名下挂单', async ({ page, customerPage }) => { + let OrderLQ = 0; + let OrderZP = 0; + await test.step('记录员工原挂单数量', async () => { + // 点击底部员工刘强 + const EmployeeLQ = page.locator('.userpanel_item').getByText(Employees.FirstShop.Employee_8.name); + if (await EmployeeLQ.isVisible()) { + await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_8.name }).click(); + await page.locator('.item_box').first().waitFor(); + OrderLQ = await page.locator('.item_box').count(); + } else { + OrderLQ = 0; + } + + // 点击底部员工周萍 + const EmployeeZP = page.locator('.userpanel_item', { hasText: Employees.FirstShop.Employee_9.name }); + if (await EmployeeZP.isVisible()) { + await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_9.name }).click(); + await page.locator('.item_box').first().waitFor(); + OrderZP = await page.locator('.item_box').count(); + } else { + OrderZP = 0; + } + }); + + await test.step('刘强挂单1', async () => { + //员工1开第1单 + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 选择第一个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_1.name }).click(); + // 选择添加员工 + await page.locator('button.staff_btn').click(); + // 选择员工:刘强 + await page + .locator('.counselor:nth-child(2)') + .locator('.check_item:nth-child(1)') + .filter({ hasText: Employees.FirstShop.Employee_8.name }) + .click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 挂单 + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + }); + + await test.step('刘强挂单2', async () => { + //员工1开第2单 + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + + // 选择第三个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_3.name }).click(); + // 选择添加员工 + await page.locator('button.staff_btn').click(); + // 选择员工:刘强 + await page + .locator('.counselor:nth-child(2)') + .locator('.check_item:nth-child(1)') + .filter({ hasText: Employees.FirstShop.Employee_8.name }) + .click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 挂单 + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + }); + + await test.step('刘强挂单3', async () => { + //员工1开第3单 + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 选择第二个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_2.name }).click(); + // 选择添加员工 + await page.locator('button.staff_btn').click(); + // 选择员工:刘强 + await page + .locator('.counselor:nth-child(2)') + .locator('.check_item:nth-child(1)') + .filter({ hasText: Employees.FirstShop.Employee_8.name }) + .click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 挂单 + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + }); + + await test.step('周萍挂单1', async () => { + //员工2开第1单 + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 选择第一个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_1.name }).click(); + // 先点击选中第一个项目 + // await page.locator('.buy_item').filter({hasText:ProjectName.Projects.Projects_1.name}).first().locator('.buy_staff').click() + // 选择添加员工 + await page.locator('button.staff_btn').click(); + // 选择员工:周萍 + await page + .locator('.counselor:nth-child(2)') + .locator('.check_row:nth-child(2) .check_item:nth-child(1)') + .filter({ hasText: Employees.FirstShop.Employee_9.name }) + .click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 挂单 + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + }); + + await test.step('周萍挂单2', async () => { + //员工2开第2单 + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 选择第二个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_2.name }).click(); + // 选择添加员工 + await page.locator('button.staff_btn').click(); + // 选择员工:周萍 + await page + .locator('.counselor:nth-child(2)') + .locator('.check_row:nth-child(2) .check_item:nth-child(1)') + .filter({ hasText: Employees.FirstShop.Employee_9.name }) + .click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 挂单 + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + // 等待挂单成功 + await page.locator('.ant-message').waitFor(); + // 判断挂单成功弹窗消失后再执行下一步 + await expect(page.locator('.ant-message')).not.toBeVisible(); + }); + + await test.step('最后校验挂单数量', async () => { + // 点击底部员工刘强 + await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_8.name }).click(); + await page.locator('.item_box').first().waitFor(); + const order = await page.locator('.item_box').all(); + expect(order.length).toBe(3 + OrderLQ); + + // 点击底部员工周萍 + await page.locator('.userpanel_item').filter({ hasText: Employees.FirstShop.Employee_9.name }).click(); + await page.locator('.item_box').first().waitFor(); + const order1 = await page.locator('.item_box').all(); + expect(order1.length).toBe(2 + OrderZP); + }); + }); + + test('使用挂单支付', async ({ page, customerPage }) => { + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 选择第一个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_1.name }).click(); + // 选择添加员工 + await page.locator('button.staff_btn').click(); + // 选择员工:吴浩 + await page + .locator('.counselor:nth-child(2)') + .locator('.check_item:nth-child(1)') + .filter({ hasText: Employees.FirstShop.Employee_10.name }) + .click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 挂单 + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + // 稍等水单出现 + await page.locator('.item_box').first().waitFor(); + // // 点击底部员工吴浩 + // 取单 + await page.locator('.item_box', { hasText: username }).waitFor(); + await page + .locator('.item_box') + .first() + .getByRole('button', { name: /^取\s单$/ }) + .click(); + //结算 + await page + .locator('.pay_btn') + .filter({ hasText: /^结\s算$/ }) + .click(); + //点击优惠券折扣 + await page.getByText('优惠券抵扣').click(); + //点击赠送优惠券 + await page.getByText('赠送优惠券').click(); + //点击定额10元券 + await page + .locator('.popup_content') + .locator('.alloytouch-target') + .filter({ hasText: Coupons.coupon.coupon_1.name }) + .click(); + //确认 + await page + .locator('.operation_btn') + .first() + .getByRole('button', { name: /^确\s定$/ }) + .click(); + //选择定额10元券 + await page.getByLabel(Coupons.coupon.coupon_1.name).first().check(); + //确认选择 + await page.getByRole('button', { name: '确认选择' }).click(); + //点击混合支付 + await page.locator('.paytype').first().getByText('混合支付').click(); + //点击现金 + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + await rightPaymentInfoItem.getByText('现金', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + //输入金额 + await page.locator('.money_discount_input').fill('100'); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //点击欠款 + await rightPaymentInfoItem.getByText('欠款', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + //输入金额 + await page.locator('.money_discount_input').fill('90'); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //点击微信 + await rightPaymentInfoItem.getByText('微信', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + //输入金额 + await page.locator('.money_discount_input').fill('100'); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //取消推送消息提醒 + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + test('已过期单据不能取单,只能删除', async ({ page }) => { + // 点击已过期 / 删除单据 + await page.getByText('已过期 / 删除单据').click(); + // 等待过期删除水单页面出来 + await page.locator('.deleteList').locator('.item_box').first().waitFor(); + // 水单都没有取单 + const PendingOrder_1 = page.locator('.deleteList').locator('.item_box'); + + await expect(PendingOrder_1.getByRole('button', { name: /^取\s单$/ })).not.toBeVisible(); + await page + .locator( + 'div:nth-child(3) > .scroller-body > .alloytouch-target > .list > div > .item > .comment > div:nth-child(2) > .touchIcon', + ) + .first() + .click(); + await expect(async () => { + await page.locator('label').filter({ hasText: '1' }).click(); + await page.locator('.ant-radio-checked').waitFor(); + }).toPass(); + await page.getByRole('button', { name: /^保\s存$/ }).click(); + await expect(page.locator('div').filter({ hasText: '删除成功' }).nth(2)).toBeVisible(); + }); +}); + +test.describe('挂单', () => { + test('成功挂单', async ({ page, createCustomer, homeNavigation, customerPage }) => { + const customer = createCustomer; + const phone = customer.phone; + const username = customer.username; + + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 选择第一个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_1.name }).click(); + // 选择第二个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_2.name }).click(); + // 选择第三个项目 + await page.locator('.project_list').filter({ hasText: ProjectName.Projects.Projects_3.name }).click(); + // 选择套餐 + await page.locator('.float_tab').getByText('套餐').click(); + // 选择第一个套餐 + await page.locator('.project_one > .item').filter({ hasText: ProjectName.SetMeal.SetMeal_1.name }).click(); + // 选择第二个套餐 + await page.locator('.project_one > .item').filter({ hasText: ProjectName.SetMeal.SetMeal_2.name }).click(); + // 点击卖品 + await page.locator('.float_tab').getByText('卖品').click(); + // 点击第一个卖品 + await page + .locator('.number_service > .project_list') + .filter({ hasText: ProjectName.Product.Product_1.name }) + .click(); + // 点击第二个卖品 + await page + .locator('.number_service > .project_list') + .filter({ hasText: ProjectName.Product.Product_2.name }) + .click(); + // 先点击选中第一个项目 + await page + .locator('.buy_item') + .filter({ hasText: ProjectName.Projects.Projects_1.name }) + .first() + .locator('.buy_staff') + .click(); + // 选择添加员工 + await page.locator('button.staff_btn').click(); + // 选择员工:陈刚 + await page + .locator('.counselor:nth-child(1)') + .locator('.check_item:nth-child(1)') + .filter({ hasText: Employees.FirstShop.Employee_6.name }) + .click(); + // 点击选择他店员工 + await page.locator('.userAdd').filter({ hasText: '选择他店员工' }).click(); + // 选择门店 + await page.locator('.label_checkbox').filter({ hasText: Feature.Store.Store_2.name }).click(); + // 确定并去选择员工 + await page.locator('.comfirm_btn').filter({ hasText: '确定并去选择员工' }).click(); + // 选择一个员工 + await page.locator('.label_checkbox').filter({ hasText: Employees.SecondShop.Employee_1.name }).click(); + // 确认 + await page + .locator('.comfirm_btn ') + .filter({ hasText: /^确\s定$/ }) + .click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 挂单 + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + + await page + .locator('.item_box') + .filter({ hasText: ProjectName.Projects.Projects_1.name }) + .first() + .locator('.open') + .click(); + + //用这里来判断是否挂单成功 + const parentLocator = page + .locator('.item_box') + .filter({ has: page.locator('.text-ellipsis', { hasText: username }) }); + await expect.soft(parentLocator).toContainText(ProjectName.Projects.Projects_1.name); + await expect.soft(parentLocator).toContainText(ProjectName.Projects.Projects_2.name); + await expect.soft(parentLocator).toContainText(ProjectName.Projects.Projects_3.name); + await expect.soft(parentLocator).toContainText(ProjectName.Product.Product_1.name); + await expect.soft(parentLocator).toContainText(ProjectName.Product.Product_2.name); + await expect.soft(parentLocator).toContainText(ProjectName.Projects.Projects_5.name); + await expect.soft(parentLocator).toContainText(ProjectName.Projects.Projects_6.name); + await expect.soft(parentLocator).toContainText(ProjectName.Projects.Projects_7.name); + await expect(parentLocator).toContainText(ProjectName.Projects.Projects_4.name); + }); + + test('已删除单据不能取单,也不能删除', async ({ page }) => { + // 找到已过期/删除单据 + await page.locator('.cash_content > .main > .top').getByText('已过期').click(); + await expect(async () => { + // 点击已删除服务 + await page.getByText(' 已删除服务 ').click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 等待过期删除水单页面出来 + await page.locator('.deleteList').locator('.item_box').first().waitFor({ timeout: 2000 }); + // 判断有没有取单和删除 + const PendingOrder = page.locator('.deleteList').locator('.item_box'); + await expect(PendingOrder.getByRole('button', { name: /^取\s单$/ })).not.toBeVisible(); + await expect( + page.locator( + 'div:nth-child(3) > .scroller-body > .alloytouch-target > .list > div > .item > .comment > div:nth-child(2) > .touchIcon', + ), + ).not.toBeVisible(); + }).toPass(); + }); + + test('微信/支付宝收款', async ({ page, homeNavigation, baseURL }) => { + // 点击收款 + await page + .locator('.right_view') + .getByRole('button', { name: /^收\s款$/ }) + .click(); + //输入金额 + await page.getByPlaceholder('请输入金额').fill('1'); + //点击微信 + await page.locator('li').filter({ hasText: '微信' }).click(); + const filePath = path.resolve(__dirname, '@/imgs/payqrcode.jpg'); + console.log(filePath); + await expect(async () => { + await page.locator('.qrcode').click(); + await page.locator('.qrcode').screenshot({ path: filePath }); + }).toPass(); + + /**@type {string} 解析后的网址 */ + let result = await decodeQR(filePath); + console.log(result); + + let url_1 = new URL(result); + // 分割成数组 + let pathParts = url_1.pathname.split('/'); + // 获取数组的最后一个元素 + let lastPart = pathParts[pathParts.length - 1]; + console.log(lastPart); + + //点击支付宝 + await page.locator('.top > .icon').click(); + await page.locator('li').filter({ hasText: '支付宝' }).click(); + await page.locator('.top > .icon').click(); + + await page.goto(result); + await page.locator('.amount').waitFor(); + let amount = await page.locator('.amount').textContent(); + expect('1.00').toBe(amount); + + //重新进入慧来客 + await page.goto(baseURL ?? ''); + await expect(page.getByRole('button', { name: /开\s单/ })).toBeVisible(); + await homeNavigation.gotoModule('流水'); + await page.locator('.top_tab').getByText('收款记录').click(); + await page + .locator('.m-table__body-wrapper tbody .m-table-cell', { hasText: '哆啦宝' }) + .first() + .waitFor({ timeout: 2000 }); + }); +}); + +test.describe('收银-房态', () => { + test('预约选床位', async ({ page, homeNavigation, createCustomer, cashierRoomPage, appointmentPage }) => { + const customer = createCustomer; + // 获取当前时间 + const setAppointment = appointmentPage.getAppointmentTimesAvailable(); + const employee = Employees.FirstShop.Employee_6; + + await test.step('进行员工预约,进入预约界面', async () => { + await homeNavigation.gotoModule('预约'); + await expect(page.locator('.room_table .tr .name_th', { hasText: employee.name })).toBeVisible(); + await page.locator('.room_table .tr .name_th', { hasText: employee.name }).scrollIntoViewIfNeeded(); + await page.locator('.times_table td', { hasText: setAppointment }).scrollIntoViewIfNeeded(); + // 打开预约选择窗口 + await appointmentPage.openAppointmentCell(employee.name); + await expect(page.locator('.popup_content', { hasText: '选择操作' })).toBeVisible(); + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + await expect(page.getByText('选择会员')).toBeVisible(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .member_list_li .phone', { hasText: customer.phone }).click(); + await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约'); + await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone); + }); + + await test.step('进入床位选择,选择床位', async () => { + await page.getByText('请选择床位').click(); + await page.getByRole('cell', { name: '按摩房' }).waitFor(); + await page.locator('.canChoose', { hasText: '选择' }).first().click(); + await page.getByRole('button', { name: '确认新建' }).click(); + + await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible(); + }); + + await test.step('进入收银-房态,查看顾客房态', async () => { + // 进入房态页面 + await homeNavigation.gotoModule('收银'); + await page.getByText('房态').click(); + await page.getByRole('cell', { name: '按摩房' }).waitFor(); + // 判断顾客房态状态 + await cashierRoomPage.checkStatus(customer, 'wait'); + // 打开顾客详情页面 + await page.getByText(customer.username).click(); + await page.locator('.ant-tabs-tab-active').filter({ hasText: '基本资料' }).waitFor(); + await page.locator('.close_icons').click(); + + // 返回预约板块 + await homeNavigation.gotoModule('预约'); + await page + .locator('.appointment') + .filter({ has: page.locator('.user_phone', { hasText: customer.phone }) }) + .locator('.user_name_info') + .click(); + await page.locator('.info_center .title').waitFor(); + await page.locator('.btn_box_btnStyle', { hasText: '取消预约' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '取消预约成功' })).toBeVisible(); + }); + }); + + test('开单选床位', async ({ page, homeNavigation, createCustomer, customerPage }) => { + const customer = createCustomer; + + await test.step('开卡', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.locator('.number_service').waitFor(); + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + // 选择会员卡A + await page.locator('.memberCard_box > .needsclick').getByText('会员卡A').click(); + // 结算 + await page.getByRole('button', { name: '去结算' }).click(); + // 选择现金支付 + await page.getByText('现金').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.locator('.modal_title', { hasText: '会员协议签署确认' }).waitFor(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await page.getByRole('button', { name: '转寄存' }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 关闭收银界面 + await page.locator('use').first().click(); + }); + + await test.step('开单选择床位', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.getByRole('button', { name: '关联床位' }).click(); + await page.locator('.left', { hasText: '安排床位' }).waitFor(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await expect(async () => { + const noRoom = page.locator('.noRoom', { hasText: '门店暂未配置房间/床位信息' }); + if (await noRoom.isVisible()) { + await page.locator('.top_control .close').click(); + await page.getByRole('button', { name: '关联床位' }).click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await expect(noRoom).not.toBeVisible(); + } + }).toPass(); + + // 点击选择床位 + const allRoom = page.locator('.room_beds_table .room_table tr'); + const count = await allRoom.count(); + for (let i = 0; i < count; i++) { + const tr = allRoom.nth(i); + const userProject = tr.getByRole('button', { name: /^选\s择$/ }); + if (await userProject.isVisible()) { + await userProject.click(); + await page.getByText('30分钟', { exact: true }).click(); + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await page.getByText('房态').click(); + await expect(page.locator('.room_board')).toBeVisible(); + // 判断该会员是否有服务中的床位 + await expect.soft(page.locator('.doing', { hasText: customer.username })).toBeVisible(); + await page.locator('.tab_item', { hasText: '挂单' }).click(); + // await page.reload(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await page + .locator('.item_box', { hasText: customer.username }) + .first() + .getByRole('button', { name: /^取\s单$/ }) + .click(); + // 点击取消关联床位 + await expect(page.locator('.right_control_btn .close_icon')).toBeVisible(); + await page.locator('.right_control_btn .close_icon').click(); + await page.getByRole('button', { name: '确 认' }).click(); + await page.locator('#cart_bottom_btn').getByText('挂单').click(); + break; + } else { + console.log('无房间,测试失败'); + } + } + }); + }); + + test('占用床位', async ({ + page, + homeNavigation, + createCustomer, + cashierRoomPage, + customerPage, + appointmentPage, + }) => { + const customer = createCustomer; + + const employee = Employees.FirstShop.Employee_6; + + await homeNavigation.gotoModule('收银'); + await page.locator('#frame_detail div').filter({ hasText: '房态' }).nth(3).click(); + await page.locator('.room_table').first().waitFor(); + const setAppointment = appointmentPage.getAppointmentTimesAvailable(); // 获取当前时间 + const cede = page.locator('.left_table td'); + + // 找到当前时间的行数 + const nowRowTime = await cede.allInnerTexts().then(text => text.indexOf(setAppointment)); + // 获取第几列没被占用 + const nowRowRoom = await page + .locator(`.room_board .tr .td:nth-child(${nowRowTime})`) + .allInnerTexts() + .then(text => text.findIndex(item => !item.includes('占用'))); + + if (nowRowRoom === -1 || nowRowTime === -1) { + throw new Error('未找到可用床位或时间'); + } + + const startElement = page.locator('.room_board .tr').nth(nowRowRoom).locator('.td').nth(nowRowTime); + + // 确保元素存在 + if (!startElement) { + console.error('元素未找到'); + } + + // 边界框 + const startRect = await startElement.boundingBox(); + + if (!startRect) { + throw new Error('元素边界框未获取到'); + } + const startX = startRect.x + startRect.width / 2; + const startY = startRect.y + startRect.height / 2; + await expect(async () => { + // 占用床位 + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(startX, startY); + await page.mouse.up(); + + const Occupied = page.locator('.ant-message', { hasText: '该时间段已被占用' }); + if (await Occupied.isVisible()) { + const startX = startRect.x + startRect.width * 1.5; + const startY = startRect.y + startRect.height / 2; + + // 占用床位 + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(startX, startY); + await page.mouse.up(); + + const startElements = page + .locator('.room_board .tr') + .nth(nowRowRoom + 1) + .locator('.td') + .nth(nowRowTime); + await expect(startElements.locator('.occupy_text', { hasText: /^占用$/ })).toBeVisible({ + timeout: 2000, + }); + } else { + await expect(startElement.locator('.occupy_text', { hasText: /^占用$/ })).toBeVisible({ + timeout: 2000, + }); + } + }).toPass(); + + await test.step('进行员工预约,进入预约界面', async () => { + await homeNavigation.gotoModule('预约'); + await page.locator('.room_table .tr .name_th', { hasText: employee.name }).waitFor(); + await page.locator('.room_table .tr .name_th', { hasText: employee.name }).scrollIntoViewIfNeeded(); + await page.locator('.times_table td', { hasText: setAppointment }).scrollIntoViewIfNeeded(); + // 打开预约选择窗口 + await appointmentPage.openAppointmentCell(employee.name); + await page.locator('.popup_content', { hasText: '选择操作' }).waitFor(); + await appointmentPage.operationAppointment(AppointmentOperation.ADD_APPOINT); + await page.getByText('选择会员').waitFor(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await expect(page.locator('.newAppointmentContent .header_title')).toContainText('新建预约'); + await expect(page.locator('.newAppointmentContent .content .phone')).toContainText(customer.phone); + }); + + await test.step('进入床位选择,选择床位', async () => { + await page.getByText('请选择床位').click(); + await page.getByRole('cell', { name: '按摩房' }).waitFor(); + // 尝试点击被占用,可能点不到 + await expect(async () => { + await startElement.click(); + const time = page.locator('.ant-message', { hasText: '请选择预约的时段!' }); + if (!(await time.isVisible())) { + const BeOccupied = page.locator('.ant-message', { + hasText: '床位已被占用,请重新选择', + }); + await expect.soft(BeOccupied).toBeVisible({ timeout: 2000 }); + } else { + await page.locator('.canChoose', { hasText: '选择' }).first().click(); + } + await page.locator('.canChoose', { hasText: '选择' }).first().click(); + }).toPass(); + await expect(async () => { + await page.getByRole('button', { name: '确认新建' }).click(); + await expect(page.locator('.ant-message', { hasText: '预约成功' })).toBeVisible({ + timeout: 3000, + }); + }).toPass(); + }); + + await test.step('进入收银-房态,查看顾客房态', async () => { + // 进入房态页面 + await homeNavigation.gotoModule('收银'); + await page.getByText('房态').click(); + await page.getByRole('cell', { name: '按摩房' }).waitFor(); + // 判断顾客房态状态 + await cashierRoomPage.checkStatus(customer, 'wait'); + // 打开顾客详情页面 + await page.getByText(customer.username).click(); + await expect(page.locator('.ant-tabs-tab-active').filter({ hasText: '基本资料' })).toBeVisible(); + await page.locator('.close_icons').click(); + + // 取消占用 + await startElement + .locator('.occupy_text', { hasText: /^占用$/ }) + .first() + .click(); + await page.getByText('取消占用').first().click(); + await page.getByRole('button', { name: '确 认' }).click(); + await expect(page.locator('div').filter({ hasText: '取消占用成功!' }).nth(2)).toBeVisible(); + + // 返回预约板块 + await homeNavigation.gotoModule('预约'); + await page + .locator('.appointment') + .filter({ has: page.locator('.user_phone', { hasText: customer.phone }) }) + .locator('.user_name_info') + .click(); + await page.locator('.info_center .title').waitFor(); + await page.locator('.btn_box_btnStyle', { hasText: '取消预约' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message', { hasText: '取消预约成功' }).waitFor(); + }); + }); + + test('取消占用床位', async ({ page, appointmentPage }) => { + const remark = faker.helpers.fromRegExp(/1[3-9][0-9]{6}/); + const remark2 = faker.helpers.fromRegExp(/1[3-9][0-9]{6}/); + await page.locator('#frame_detail div').filter({ hasText: '房态' }).nth(3).click(); + await page.locator('.room_table').first().waitFor(); + const setAppointment = appointmentPage.getAppointmentTimesAvailable(); // 获取当前时间 + const cede = page.locator('.left_table td'); + // 找到当前时间的行数 + /**@type {number} 找到当前时间的行数 */ + const nowRowTime = await cede.allInnerTexts().then(text => { + return text.findIndex(item => item === setAppointment) + 2; + }); + // 获取第几列没被占用 + const nowRowRoom = await page + .locator(`.room_board .tr .td:nth-child(${nowRowTime})`) + .allInnerTexts() + .then(text => text.findIndex(item => !item.includes('占用'))); + + if (nowRowRoom === -1) { + throw new Error('未找到可用床位或时间'); + } + + const startElement = page.locator('.room_board .tr').nth(nowRowRoom).locator('.td').nth(nowRowTime); + + // 确保元素存在 + if (!startElement) { + console.error('元素未找到'); + } + + // 边界框 + const startRect = await startElement.boundingBox(); + + if (!startRect) { + throw new Error('元素边界框未获取到'); + } + + const startX = startRect.x + startRect.width / 2; + const startY = startRect.y + startRect.height / 2; + + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(startX, startY); + await page.mouse.up(); + + await startElement + .locator('.occupy_text', { hasText: /^占用$/ }) + .first() + .click(); + await page.getByText('添加备注').click(); + await page.getByPlaceholder('请输入备注').fill(remark); + await page.getByRole('button', { name: '确 认' }).click(); + await page.locator('.occupy_remark', { hasText: remark }).click(); + await page.getByText('修改备注').first().click(); + await page.getByPlaceholder('请输入备注').fill(remark2); + await page.getByRole('button', { name: '确 认' }).click(); + await page.locator('.occupy_remark', { hasText: remark2 }).click(); + await page.getByText('返回').click(); + await page.locator('.occupy_remark').click(); + await page.getByText('取消占用').first().click(); + await page.getByRole('button', { name: '确 认' }).click(); + await expect(page.locator('div').filter({ hasText: '取消占用成功' }).nth(2)).toBeVisible(); + }); + + test('修改占用床位', async ({ page, appointmentPage }) => { + await page.locator('#frame_detail div').filter({ hasText: '房态' }).nth(3).click(); + await page.locator('.room_table').first().waitFor(); + const setAppointment = appointmentPage.getAppointmentTimesAvailable(); // 获取当前时间 + const cede = page.locator('.left_table td'); + // 找到当前时间的行数 + // const nowRowTime = await cede.allInnerTexts().then(text => text.indexOf(setAppointment) + 3); + /**@type{number} 找到当前时间的行数 */ + const nowRowTime = await cede.allInnerTexts().then(text => { + return text.findIndex(item => item === setAppointment) + 2; + }); + // 获取第几列没被占用 + const nowRowRoom = await page + .locator(`.room_board .tr .td:nth-child(${nowRowTime})`) + .allInnerTexts() + .then(text => text.findIndex(item => !item.includes('占用'))); + + if (nowRowRoom === -1 || nowRowTime === -1) { + throw new Error('未找到可用床位或时间'); + } + + const startElement = page.locator('.room_board .tr').nth(nowRowRoom).locator('.td').nth(nowRowTime); + + // 确保元素存在 + if (!startElement) { + console.error('元素未找到'); + } + + // 边界框 + const startRect = await startElement.boundingBox(); + + if (!startRect) { + throw new Error('元素边界框未获取到'); + } + + const startX = startRect.x + startRect.width / 2; + const startY = startRect.y + startRect.height / 2; + + const startedX = startRect.x + startRect.width * 1.5; + const startedY = startRect.y + startRect.height / 2; + + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.up(); + await expect(startElement.locator('.occupy_text', { hasText: /^占用$/ })).toBeVisible(); + + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(startedX, startedY); + await page.mouse.up(); + + const startElemented = page + .locator('.room_board .tr') + .nth(nowRowRoom + 1) + .locator('.td') + .nth(nowRowTime); + // toPass 可能点不出取消占用 + await expect(async () => { + await startElemented + .locator('.occupy_text', { hasText: /^占用$/ }) + .first() + .click(); + await page.getByText('取消占用').first().waitFor({ timeout: 2000 }); + }).toPass(); + await page.getByText('取消占用').first().click(); + await page.getByRole('button', { name: '确 认' }).click(); + await expect(page.locator('div').filter({ hasText: '取消占用成功!' }).nth(2)).toBeVisible(); + }); +}); + +test.describe('收银-开单&结算', () => { + test('开单-反结算-撤单', async ({ + page, + homeNavigation, + createCustomer, + customerPage, + wasteBookBusinessRecordPage, + numberInput, + }) => { + // 定义一个随机单号 + const randomBillNo1 = faker.helpers.fromRegExp(/1[3-9][0-9]{8}/); + const randomBillNo2 = faker.helpers.fromRegExp(/1[3-9][0-9]{9}/); + + const customer = createCustomer; + const username = customer.username; + const phone = customer.phone; + + // 单据定位器 + const $$billLeft = page.locator('.m-table__fixed-left .m-table-cell .bill .m-dropdown-link'); + const $$billBody = page.locator('.m-table__body-wrapper tbody .m-table-cell'); + + await test.step('创建会员开单', async () => { + // 项目1和项目2 + const project1 = ProjectName.Projects.Projects_15; + const project2 = ProjectName.Projects.Projects_17; + // 员工1和员工2 + const employee1 = Employees.FirstShop.Employee_6.name; + const employee2 = Employees.FirstShop.Employee_7.name; + + // 现金 + const cash = 50; + // 欠款 + const arrears = 50; + // 微信 + const weChat = 100; + + await test.step('开单购买项目,选择员工结算', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + + // 选择项目1 + await page.getByText(project1.num).click(); + // 购买项目添加员工1 + await expect(async () => { + await page.locator('.buy_staff').last().click(); + await expect(page.locator('.header', { hasText: '员工业绩' })).toBeVisible({ timeout: 2000 }); + }).toPass(); + await page.getByLabel(employee1).first().check(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + // 选择项目2 + await page.getByText(project2.num).click(); + // 点击添加员工2 + await expect(async () => { + await page.locator('.buy_staff').last().click(); + await expect(page.locator('.header', { hasText: '员工业绩' })).toBeVisible({ timeout: 2000 }); + }).toPass(); + await page.getByLabel(employee2).first().check(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + // 结算 + await page.getByText(/结\s算/).click(); + + // 输入随机单号 + await page.locator('.input165').click(); + await page.getByPlaceholder('请输入内容').fill(randomBillNo1); + + // 确认 + await page.locator('.tools_icon').last().click(); + // 点击混合支付 + await page.locator('.paytype').getByText('混合支付').click(); + // 制定选择项目A + await page + .locator('.itemlist_radio_group') + .getByText(/^星灿明眸奢享套\s/) + .click(); + // 输入现金金额 + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + await rightPaymentInfoItem.getByText('现金', { exact: true }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.money_discount_input').fill(`${cash}`); + await page.locator('.sure .tools_icon').click(); + + // 输入欠款金额 + await rightPaymentInfoItem.getByText('欠款', { exact: true }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.money_discount_input').fill(`${arrears}`); + + // 输入微信金额 + await page.locator('.sure .tools_icon').click(); + await rightPaymentInfoItem.getByText('微信', { exact: true }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.money_discount_input').fill(`${weChat}`); + await page.locator('.sure .tools_icon').click(); + + await page.locator('.additional').filter({ hasText: '关键数据配置' }).getByText('未分配').click(); + await page.locator('.popup_content').getByLabel(employee1).check(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + }); + + // 获取项目1定位器 + const $project1Result = page.locator('.m-detailComponent_report .items_name .name_txt').first(); + // 获取项目2定位器 + const $project2Result = page.locator( + '.m-detailComponent_report .main-table-body_tr:nth-child(2) .items_name .name_txt', + ); + // 获取员工1定位器 + const $Employee1Result = page.locator('.m-detailComponent_report .billUser_item .userName').first(); + // 获取员工2定位器 + const $Employee2Result = page.locator( + '.m-detailComponent_report .main-table-body_tr:nth-child(2) .billUser_item .userName', + ); + // 获取总金额定位器 + const $TotalAmount = page.locator('.m-detailComponent_consume_title .amount'); + // 获取现金定位器 + const $JudgmentArrears = page.locator('.m-detailComponent_consume_title .overdraft'); + + await test.step('在营业记录、业绩流水、对账流水查看单据明细', async () => { + await homeNavigation.gotoModule('流水'); + // 在营业记录中查询 + // 等待加载完成 + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 打开单据明细 + await $$billLeft.getByText(randomBillNo1).first().click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await page.locator('.m-receiptDetail_tip', { hasText: '单据明细' }).waitFor(); + // 对比第一个输入的项目 + await expect.soft($project1Result).toContainText(project1.name); + // 对比第二个输入的项目 + await expect.soft($project2Result).toContainText(project2.name); + // 对比第一个员工为陈刚 + await expect.soft($Employee1Result).toContainText(employee1); + // 对比第二个员工为张伟 + await expect.soft($Employee2Result).toContainText(employee2); + // 判断总金额 + await expect.soft($TotalAmount).toContainText(`${cash + arrears + weChat}`); + // 判断欠款 + await expect($JudgmentArrears).toContainText(`${arrears}`); + + await page.reload(); + // 点击流水 + await homeNavigation.gotoModule('流水'); + // 在业绩流水中查询 + await wasteBookBusinessRecordPage.gotoSubPage('业绩流水'); + // 打开单据明细 + await $$billLeft.getByText(randomBillNo1).first().click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await page.locator('.m-receiptDetail_tip', { hasText: '单据明细' }).waitFor(); + // 对比第一个输入的项目 + await expect.soft($project1Result).toContainText(project1.name); + // 对比第二个输入的项目 + await expect.soft($project2Result).toContainText(project2.name); + // 对比第一个员工为陈刚 + await expect.soft($Employee1Result).toContainText(employee1); + // 对比第二个员工为张伟 + await expect.soft($Employee2Result).toContainText(employee2); + // 判断总金额 + await expect.soft($TotalAmount).toContainText(`${cash + arrears + weChat}`); + // 判断欠款 + await expect($JudgmentArrears).toContainText(`${arrears}`); + + await page.reload(); + // 点击流水 + await homeNavigation.gotoModule('流水'); + // 在对账流水中查询 + await wasteBookBusinessRecordPage.gotoSubPage('对账流水'); + // 打开单据明细 + await $$billBody.getByText(randomBillNo1).first().click(); + await expect(page.locator('.loading_container')).toBeHidden(); + await page.locator('.m-receiptDetail_tip', { hasText: '单据明细' }).waitFor(); + // 对比第一个输入的项目 + await expect.soft($project1Result).toContainText(project1.name); + // 对比第二个输入的项目 + await expect.soft($project2Result).toContainText(project2.name); + // 对比第一个员工为陈刚 + await expect.soft($Employee1Result).toContainText(employee1); + // 对比第二个员工为张伟 + await expect.soft($Employee2Result).toContainText(employee2); + // 判断总金额 + await expect.soft($TotalAmount).toContainText(`${cash + arrears + weChat}`); + // 判断欠款 + await expect($JudgmentArrears).toContainText(`${arrears}`); + }); + }); + + await page.reload(); + + await test.step('反结算', async () => { + const project1 = ProjectName.Projects.Projects_15.name; + const project2 = ProjectName.Projects.Projects_9.name; + const employee1 = Employees.FirstShop.Employee_6.name; + + await homeNavigation.gotoModule('流水'); + await wasteBookBusinessRecordPage.gotoSubPage('营业记录'); + // 点击指定单号 + await $$billLeft.getByText(randomBillNo1).first().click(); + // 点击反结算 + await page.locator('.oversized', { hasText: '反结算' }).click(); + await page.locator('.custom_content').waitFor(); + await page.locator('.buy_item .buy_name .del_btn').last().click(); + // 选择项目1 + await expect(async () => { + await page.getByText(ProjectName.Projects.Projects_9.num).click(); + await page.locator('#buyList').getByText('青春雅致套').waitFor(); + }).toPass(); + + await expect(async () => { + const employeeLogo = page.locator('#buyList').getByRole('button').nth(1); + if (!(await employeeLogo.isVisible())) { + await page.locator('.buy_staff').last().click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + await page.locator('.header', { hasText: '员工业绩' }).waitFor({ timeout: 2000 }); + } else { + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + await page.locator('.header', { hasText: '员工业绩' }).waitFor({ timeout: 2000 }); + } + }).toPass(); + + // 选择员工1 + const employee2 = Employees.FirstShop.Employee_4.name; + await page.getByText(Employees.FirstShop.Employee_4.name).first().click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 修改数量 + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(2); + await numberInput.confirmValue(); + const quantity = (await page.locator('.buy_number').last().innerText()).trim(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + + // 修改单号 + await page.getByPlaceholder('不输入则自动生成订单号').click(); + await numberInput.setString(randomBillNo2); + await numberInput.confirmValue(); + + // 使用银联支付 + await page.getByText('银联', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + + // 对比第一个项目 + await expect.soft(page.locator('.items_name .name_txt').first()).toContainText(project1); + // 对比第二个项目 + await expect.soft(page.locator('.items_name .name_txt').last()).toContainText(project2); + // 对比员工 + await expect + .soft( + page + .locator('.m-detailComponent_report .main-table-body_tr:nth-child(1) .billUser_item .userName') + .first(), + ) + .toContainText(employee1); + await expect + .soft( + page + .locator('.m-detailComponent_report .main-table-body_tr:nth-child(2) .billUser_item .userName') + .last(), + ) + .toContainText(employee2); + // 对比数量 + await expect.soft(page.locator('.m-detailComponent-cell .num').last()).toContainText(quantity); + // 对比总金额 + const project1ResultCollect = ProjectName.Projects.Projects_15.Price; + const project2ResultCollect = ProjectName.Projects.Projects_9.Price; + await expect(page.locator('.m-detailComponent_consume_title .amount')).toContainText( + `${Number(project1ResultCollect) + Number(project2ResultCollect) * Number(quantity)}`, + ); + }); + + await page.reload(); + + await test.step('撤单', async () => { + // 进入营业记录页面 + await homeNavigation.gotoModule('流水'); + + // 点击指定单号 + await $$billLeft.getByText(randomBillNo2).click(); + // 点击撤单 + await page.locator('.ant-btn', { hasText: '撤单' }).click(); + // 点击输入备注 + await page.locator('.label_text').last().click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('撤单'); + await page.locator('.saveCheck').waitFor(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 点击撤单查看 + await page.locator('.search_select').first().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '已撤单' }).click(); + // 判断撤的单子是否还在 + await expect($$billLeft.getByText(randomBillNo2).first()).toBeVisible(); + }); + }); + + test('开卡-使用卡金和赠金-充值卡金', async ({ + page, + homeNavigation, + createCustomer, + customerPage, + numberInput, + }) => { + const c = createCustomer; + const username = c.username; + const phone = c.phone; + + await test.step('开卡', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + await page.locator('.number_service').waitFor(); + // 点击开卡 + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + const CardTypeA = '会员卡A'; + // 选择会员卡A + await page.locator('.memberCard_box > .needsclick').getByText(CardTypeA, { exact: true }).click(); + // 结算 + await page.getByRole('button', { name: '去结算' }).click(); + // 选择现金支付 + await page.getByText('现金').click(); + // 取消推送消费提醒 + await page.getByLabel('推送消费提醒').uncheck(); + // 取消结算签字 + await page.getByLabel('结算签字').uncheck(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + // 寄存卖品 + await page.getByRole('button', { name: '转寄存' }).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 关闭收银界面 + await page.locator('use').first().click(); + // 点击顾客 + await page.locator('.link_item_member').click(); + // 输入会员信息 + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + await customerPage.openCustomerDetail(username, phone); + // 校验会员卡金 赠送金是否准确 + await expect.soft(page.locator('.membercard_box .card_name .name').first()).toContainText(CardTypeA); + // 卡金对比 + await expect.soft(page.locator('.balance_right').first()).toContainText('1000'); + // 赠送金对比 + await expect(page.locator('.membercard_box .bonuses').first()).toContainText('100'); + }); + + await page.reload(); + + await test.step('使用卡金和赠金', async () => { + //使用卡金和赠金 + await page.reload(); + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 选择项目A + await page.getByText(ProjectName.Projects.Projects_15.num).click(); + // 选择项目B + await page.getByText(ProjectName.Projects.Projects_17.num).click(); + // 结算 + await page.getByText(/^结\s算$/).click(); + // 点击优惠券 + await page.getByText('张优惠券可用').click(); + // 点一张优惠券 + await page.locator('.item > .name').first().click(); + // 确认选择 + await page.getByRole('button', { name: '确认选择' }).click(); + // 点击混合支付 + await page.getByText('混合支付').click(); + // 选择卡金支付 + await page.getByText('卡金').nth(4).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + // 输入金额 + await numberInput.setValue(100); + await numberInput.confirmValue(); + // 选择赠送金支付 + await page.getByText('赠金').nth(2).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + // 输入金额 + await numberInput.setValue(90); + await numberInput.confirmValue(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.getByRole('button', { name: /^结\s算$/ })).not.toBeVisible(); + }); + + await page.reload(); + + await test.step('充值卡金', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + + await page.locator('.membercard_box .name_row .balance').waitFor(); + const CardAmount = KeepOnlyNumbers(await page.locator('.membercard_box .name_row .balance').innerText()); + + // 点击充值 + await page.getByRole('button', { name: /^充\s值$/ }).click(); + // 点击充值总额 + await page.locator('span > .touchIcon').first().click(); + // 输入金额 + const TopUp = 1000; + await page.getByPlaceholder('请输入内容').fill(`${TopUp}`); + // 确认金额(勾勾) + await page.locator('.tools_icon').last().click(); + // 点击现金 + await page.getByText('现金').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 点击结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + // 关闭收银界面 + await page.locator('use').first().click(); + await homeNavigation.gotoModule('顾客'); + // 搜索会员 + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 点击会员卡 + await page.locator('.user_name', { hasText: username }).last().click(); + // 校验会员卡金是否准确 + const BalanceSumTotal = Number(CardAmount) + TopUp; + await expect(page.locator('.balance_right').first()).toContainText(`${BalanceSumTotal}`); + + await page.getByRole('tab', { name: '流水' }).click(); + await page.getByText('会员卡记录').click(); + await page.locator('.flow_list').waitFor(); + // 记录 + await expect(page.locator('.flow_section td:nth-child(5)').first()).toContainText(`${TopUp}`); + }); + }); + + test('还欠款', async ({ page, homeNavigation, createCustomer, customerPage }) => { + // 创建顾客 + const c = createCustomer; + // 获取姓名、手机号、档案号 + const username = c.username; + const phone = c.phone; + + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 点击开卡 + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + // 选择会员卡A + await page.locator('.memberCard_box > .needsclick').getByText(CardType.CardTypeList.CardType_1.name).click(); + // 结算 + await page.getByRole('button', { name: '去结算' }).click(); + //点击混合支付 + await page.locator('.paytype').first().getByText('混合支付').click(); + //点击现金 + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + await rightPaymentInfoItem.getByText('现金', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + //输入金额 + await page.locator('.money_discount_input').fill('500'); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //点击欠款 + await rightPaymentInfoItem.getByText('欠款', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + //输入金额 + await page.locator('.money_discount_input').fill('100'); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //点击微信 + await rightPaymentInfoItem.getByText('微信', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + //输入金额 + await page.locator('.money_discount_input').fill('400'); + //确认金额 + await page.locator('.sure .tools_icon').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + //结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /^跳\s过$/ }).click(); + await page.locator('.ant-message', { hasText: '结算成功' }).waitFor(); + await page.getByRole('button', { name: '不寄存' }).click(); + await page.locator('.close').first().click(); + + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + // 点击还款 + await page.locator('.comment > div:nth-child(2) > button:nth-child(2)').first().click(); + // 点击修改金额 + await page.locator('.touchIcon').first().waitFor(); + await page.locator('.touchIcon').first().click(); + // 输入金额 + await page.getByPlaceholder('请输入内容').fill('50'); + // 确认 + await page.locator('.tools_icon').last().click(); + // 选择现金支付 + await page.getByText('现金').click(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.locator('.ant-message', { hasText: '结算成功' }).waitFor(); + + // 点击顾客 + await homeNavigation.gotoModule('顾客'); + // 输入信息 + await customerPage.searchCustomer(phone); + await customerPage.selectSearchCustomer(username); + + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: username }).last().click(); + await page.getByRole('tab', { name: '流水' }).click(); + await page.locator('label').filter({ hasText: '欠还款记录' }).click(); + // 总欠款100 + await expect(page.locator('.total_debt', { hasText: '100' })).toBeVisible(); + // 待还款50 + await expect(page.locator('.stay_paid', { hasText: '50' })).toBeVisible(); + }); +}); diff --git a/tests/touch/boss_customer.spec.ts b/tests/touch/boss_customer.spec.ts new file mode 100644 index 0000000..1077543 --- /dev/null +++ b/tests/touch/boss_customer.spec.ts @@ -0,0 +1,3600 @@ +// @ts-check +import { test, expect } from '@/fixtures/boss_common.js'; +import { faker } from '@faker-js/faker/locale/zh_CN'; +import { Customer } from '@/utils/customer'; +import { waitSpecifyApiLoad, convertAmountText, waitStable } from '@/utils/utils.js'; +import { Employees, ProjectName } from '@/fixtures/userconfig.js'; +import path from 'path'; +import fs from 'fs'; +import { HomeNavigation } from '@/pages/homeNavigationPage.js'; +import { CustomerPage } from '@/pages/customer/customerPage.js'; + +test.describe('顾客通用', () => { + test.describe('快捷查询', () => { + const customer = new Customer(1, 1, { + archive: 'ABC' + faker.string.alpha(3), + employees: [ + { + name: '周慧', + level: '咨询师', + }, + ], + }); + /**@type {import('playwright').BrowserContext} */ + let browserContext; + /**@type {import('playwright').Page} */ + let page; + let homeNavigation; + let customerPage; + test.beforeAll(async ({ browser, baseURL }) => { + browserContext = await browser.newContext(); + page = await browserContext.newPage(); + if (!baseURL) throw new Error('baseURL is required'); + await page.goto(baseURL); + await page.waitForLoadState('networkidle'); + await page.reload(); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + homeNavigation = new HomeNavigation(page); + customerPage = new CustomerPage(page); + await homeNavigation.gotoModule('顾客'); + await customerPage.createCustomer(customer); + }); + + test.afterAll(async () => { + homeNavigation = new HomeNavigation(page); + customerPage = new CustomerPage(page); + await homeNavigation.gotoModule('顾客'); + await customerPage.setInvalidCustomer(customer); + await page.close(); + await browserContext.close(); + }); + + test('精准查询', async ({ page, homeNavigation, customerPage }) => { + /**@type{Set} 搜索历史 */ + let historySet = new Set(); + + // 去创建顾客 + const username = customer.username; + const phone = customer.phone; + const archive = customer.archive; + + await test.step('根据名字第一个字符进行搜索', async () => { + await homeNavigation.gotoModule('顾客'); + const searchStr = username.substring(0, Math.ceil(username.length / 2)); + await customerPage.searchCustomer(searchStr); + + await page.locator('.alertBox .close').waitFor(); + // 判断顾客存在 + await expect(page.locator('.custom_content', { hasText: username })).toBeVisible(); + // 添加到搜索历史 + historySet.add(searchStr); + await page.locator('.alertBox .close .anticon-close').click(); + }); + + await test.step('根据手机号进行搜索', async () => { + await customerPage.searchCustomer(phone); + + await page.locator('.alertBox .close').waitFor(); + // 判断顾客存在 + await expect(page.locator('.custom_content', { hasText: username })).toBeVisible(); + // 添加到搜索历史 + historySet.add(phone); + await page.locator('.alertBox .close .anticon-close').click(); + }); + + await test.step('根据档案号进行搜索', async () => { + await customerPage.searchCustomer(archive); + + await page.locator('.alertBox .close').waitFor(); + // 判断顾客存在 + await expect(page.locator('.custom_content', { hasText: username })).toBeVisible(); + // 添加到搜索历史 + historySet.add(archive); + await page.locator('.alertBox .close .anticon-close').click(); + }); + + await test.step('查询搜索历史', async () => { + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').click(); + await page.locator('.historyALertBox_title', { hasText: '搜索历史' }).waitFor(); + // 判断搜索历史是否存在 + await expect(page.locator('.historyMemberList_item')).toContainText([@.historySet].reverse()); + }); + }); + + test('顾客概要', async ({ page, homeNavigation, customerPage, tablePage }) => { + // 第一个顾客 + const $firstCustomer = tablePage.fixedLeftTable.locator('tr td:nth-child(2)').locator('.name ').first(); + await test.step('选择部门一,查看顾客详情', async () => { + await homeNavigation.gotoModule('顾客'); + await page.locator('.shop-picker-store').click(); + await page.getByLabel('医美部').uncheck(); + await page.getByLabel('美容部').check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + // 点击会员 + await page.locator('.sub_icon').first().click(); + await page.locator('.m-table__icon__warp').waitFor({ state: 'hidden' }); + // 打开顾客详情 + await $firstCustomer.click(); + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断该会员所属部门是否有美容部 + await expect(page.locator('.basic_box .item_txt1', { hasText: '美容部' })).toBeVisible(); + // 关闭基础资料 + await customerPage.closeCustomerDetail(); + await page.locator('.store_mark').click(); + await page.locator('.store_mark').waitFor({ state: 'hidden' }); + }); + + await test.step('选择员工,查看顾客详情', async () => { + await page.locator('.shop-picker-store').click(); + await page.getByLabel('医美部').uncheck(); + await page.getByLabel('美容部').uncheck(); + await page.getByLabel('周慧').check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 打开顾客详情 + await $firstCustomer.click(); + await expect(page.locator('.employees_txt')).toContainText('周慧'); + }); + }); + + test('顾客分配', async ({ page, homeNavigation, customerPage, tablePage }) => { + // 第一个顾客 + const $firstCustomer = tablePage.fixedLeftTable.locator('tr td:nth-child(2)').locator('.name ').first(); + await test.step('顾客分配', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客分配'); + await page.locator('.assign_info').waitFor(); + // 点击左上角门店 + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 员工不可见 + await expect(page.locator('.shopSelect_small_title', { hasText: '员工' })).not.toBeVisible(); + // 选择美容部 + await page.getByLabel('医美部').uncheck(); + await page.getByLabel('美容部').check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + + // 打开顾客详情 + await $firstCustomer.click(); + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断该会员所属部门是否有美容部 + await expect(page.locator('.basic_box .item_txt1', { hasText: '美容部' })).toBeVisible(); + }); + }); + + test('顾客动态', async ({ page, homeNavigation, customerPage, tablePage }) => { + // 第一个顾客 + const $firstCustomer = tablePage.fixedLeftTable.locator('tr td:nth-child(2)').locator('.name ').first(); + await test.step('选择部门一,查看顾客详情', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客动态'); + + await page.locator('.shop-picker-store').click(); + await page.getByLabel('医美部').uncheck(); + await page.getByLabel('美容部').check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await $firstCustomer.click(); + await page.locator('.basic_box .base_info').first().waitFor(); + await expect(page.locator('.basic_box .item_txt1', { hasText: '美容部' })).toBeVisible(); + await page.locator('.close_icons').click(); + await page.locator('.basic_box .base_info').waitFor({ state: 'hidden' }); + }); + + await test.step('选择部门一的员工,查看顾客详情', async () => { + await page.locator('.shop-picker-store').click(); + await page.getByLabel('医美部').uncheck(); + await page.getByLabel('美容部').uncheck(); + await page.getByLabel('周慧').check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + + // 打开顾客详情 + await $firstCustomer.click(); + // 判断分配员工 + await expect(page.locator('.employees_txt')).toContainText('周慧'); + }); + }); + + test('顾客分析', async ({ page, homeNavigation, customerPage, tablePage }) => { + // 第一个顾客 + const $firstCustomer = tablePage.fixedLeftTable.locator('tr td:nth-child(2)').locator('.name ').first(); + await test.step('选择部门一,查看顾客详情', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客分析'); + await page.locator('.shop-picker-store').click(); + await page.getByLabel('医美部').uncheck(); + await page.getByLabel('美容部').check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + // 打开顾客详情 + await $firstCustomer.click(); + await page.locator('.basic_box .base_info').first().waitFor(); + await expect(page.locator('.basic_box .item_txt1', { hasText: '美容部' })).toBeVisible(); + await page.locator('.close_icons').click(); + await expect(page.locator('.basic_box .base_info')).not.toBeVisible(); + }); + + await test.step('选择部门一的员工,查看顾客详情', async () => { + await page.locator('.shop-picker-store').click(); + await page.getByLabel('医美部').uncheck(); + await page.getByLabel('美容部').uncheck(); + await page.getByLabel('周慧').check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + // 打开顾客详情 + await $firstCustomer.click(); + // 判断分配员工 + await expect(page.locator('.employees_txt')).toContainText('周慧'); + }); + }); + }); + + test.describe('高级查询', () => { + const customer = new Customer(1, 1, { + archive: 'ABC' + faker.string.alpha(3), + employees: [ + { + name: '周慧', + level: '咨询师', + }, + ], + source: 5, + }); + /**@type {import('playwright').BrowserContext} */ + let browserContext; + /**@type {import('playwright').Page} */ + let page; + let homeNavigation; + let customerPage; + test.beforeAll(async ({ browser, baseURL }) => { + browserContext = await browser.newContext(); + page = await browserContext.newPage(); + if (!baseURL) throw new Error('baseURL is required'); + await page.goto(baseURL); + await page.waitForLoadState('networkidle'); + await page.reload(); + await page.getByRole('button', { name: /开\s单/ }).waitFor(); + homeNavigation = new HomeNavigation(page); + customerPage = new CustomerPage(page); + await homeNavigation.gotoModule('顾客'); + await customerPage.createCustomer(customer); + + await homeNavigation.gotoModule('收银'); + // 给会员开卡 + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.locator('.number_service').waitFor(); + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + // 选择会员卡A + await page.locator('.memberCard_box > .needsclick').getByText('会员卡A').click(); + // 结算,选择现金支付 + await page.getByRole('button', { name: '去结算' }).click(); + await page.getByText('现金').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await page.getByRole('button', { name: '转寄存' }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 关闭收银界面 + await page.locator('use').first().click(); + }); + + test.afterAll(async () => { + homeNavigation = new HomeNavigation(page); + customerPage = new CustomerPage(page); + await homeNavigation.gotoModule('顾客'); + await customerPage.setInvalidCustomer(customer); + await page.close(); + await browserContext.close(); + }); + + test('组合查询', async ({ page, homeNavigation }) => { + await test.step('搜索关键字-姓', async () => { + // 点击顾客 + await homeNavigation.gotoModule('顾客'); + await page.locator('.ant-btn-default', { hasText: /^高\s级$/ }).click(); + await page.locator('.shopSelect_title', { hasText: '顾客高级查询' }).waitFor(); + + // 根据所属门店点击右边搜索框 + await page + .locator('.item_sub') + .filter({ has: page.locator('.item_name', { hasText: '所属门店' }) }) + .locator('.item_val') + .click(); + // 通过点击两次全选达到所有门店不在选中状态 + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + await expect( + page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' }), + ).not.toBeVisible(); + // 选择门店一店 + await page.locator('.label_checkbox', { hasText: 'AT测试一店' }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page.locator('.ant-btn-primary', { hasText: '确定选择' }).first().click(); + // 搜索关键字'王' + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.username[0]); + // 选择拥有会员卡A + await page + .locator('.item_sub') + .filter({ has: page.locator('.item_name', { hasText: '拥有的会员卡' }) }) + .locator('.item_val') + .click(); + await page.locator('.check_box_container', { hasText: '会员卡A' }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: '会员卡A' })).toBeVisible(); + await page.locator('.ant-btn-primary', { hasText: '确定选择' }).nth(5).click(); + // 点击搜索 + await page.locator('.ant-btn-block').click(); + await expect(page.locator('.m-table__fixed-left tbody tr', { hasText: customer.phone })).toBeVisible(); + }); + }); + + test('保存查询', async ({ page, homeNavigation }) => { + await test.step('搜索关键字-姓', async () => { + // 点击顾客 + await homeNavigation.gotoModule('顾客'); + await page.locator('.ant-btn-default', { hasText: /^高\s级$/ }).click(); + await page.locator('.shopSelect_title', { hasText: '顾客高级查询' }).waitFor(); + // 点击两次全选确保所有门店默认未选 + await page + .locator('.item_sub') + .filter({ has: page.locator('.item_name', { hasText: '所属门店' }) }) + .locator('.item_val') + .click(); + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + // 判断某个门店被选中 + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + // 判断门店未被选中 + await expect( + page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' }), + ).not.toBeVisible(); + await page.locator('.label_checkbox', { hasText: 'AT测试一店' }).click(); + // 确认选中一店 + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page.locator('.ant-btn-primary', { hasText: '确定选择' }).first().click(); + // 搜索关键字 王 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.username[0]); + await page + .locator('.item_sub') + .filter({ has: page.locator('.item_name', { hasText: '拥有的会员卡' }) }) + .locator('.item_val') + .click(); + await page.locator('.check_box_container', { hasText: '会员卡A' }).click(); + // 拥有会员卡A + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: '会员卡A' })).toBeVisible(); + await page.locator('.ant-btn-primary', { hasText: '确定选择' }).nth(5).click(); + // 点击搜索 + await page.locator('.ant-btn-block').click(); + await expect(page.locator('.m-table__fixed-left tbody tr', { hasText: customer.phone })).toBeVisible(); + }); + + await test.step('保存查询', async () => { + const identificationA = faker.person.fullName(); + const identificationB = faker.person.fullName(); + await page.locator('.item-btns', { hasText: '保存查询' }).waitFor(); + await page.locator('.item-btns', { hasText: '保存查询' }).click(); + await page.locator('.modal_header').waitFor(); + await page.getByPlaceholder('请输入名称').fill(identificationA); + await expect(async () => { + await page.locator('.ant-btn-primary').last().click(); + await expect(page.locator('.ant-modal-content')).not.toBeVisible({ + timeout: 5000, + }); + }).toPass(); + + // 操作成功 + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + + await page.locator('.item-btns', { hasText: '保存查询' }).waitFor(); + await page.locator('.item-btns', { hasText: '保存查询' }).click(); + await page.locator('.modal_header').waitFor(); + await page.getByPlaceholder('请输入名称').fill(identificationB); + await page.locator('.search_tip').click(); + await expect(async () => { + await page.locator('.ant-btn-primary').last().click(); + await expect(page.locator('.ant-modal-content')).not.toBeVisible({ + timeout: 5000, + }); + }).toPass(); + + // 操作成功 + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + + // 点击高级搜索旁边三个点 + await page.locator('.ant-btn-default .anticon').click(); + await page.locator('.ant-tabs-tab').first().waitFor(); + await page.locator('.ant-tabs-tab', { hasText: '保存的搜索器' }).click(); + await page.locator('.tools_items .item').first().waitFor(); + await page.locator('.tools_items .item', { hasText: identificationA }).click(); + // 判断点击该搜索器是否能搜到该会员 + await expect(page.locator('.m-table__fixed-left tbody tr', { hasText: customer.phone })).toBeVisible(); + + // 点击高级搜索旁边三个点 + await page.locator('.ant-btn-default .anticon').click(); + await page.locator('.tools_items .item').first().waitFor(); + // 判断该搜索器有没有手机标识 + const phoneLogo = page + .locator('.tools_items .item') + .filter({ has: page.locator('.tool_name', { hasText: identificationB }) }) + .locator('.item_icon'); + await expect(phoneLogo).toBeVisible(); + }); + + await test.step('删除保存的搜索器', async () => { + // 点击高级搜索旁边三个点 上一步停留在了这里面不需要点 + await page.locator('.ant-tabs-tab', { hasText: '保存的搜索器' }).click(); + await page.locator('.tools_items .item').first().waitFor(); + // 点击右上角设置齿轮 + await page.locator('.edit_btn').click(); + // 等待保存搜索的右上角叉叉按钮出现 + await page.locator('.del_btn').first().click(); + // 点击完成 + await page.locator('.btn_ok').click(); + // 操作成功 + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + }); + + test('使用历史查询', async ({ page, homeNavigation, createCustomCustomer, customerPage }) => { + await test.step('搜索关键字-姓', async () => { + // 点击顾客 + await homeNavigation.gotoModule('顾客'); + await page.locator('.ant-btn-default', { hasText: /^高\s级$/ }).click(); + await page.locator('.shopSelect_title', { hasText: '顾客高级查询' }).waitFor(); + // 点击两次全选确保所有门店默认未选 + await page + .locator('.item_sub') + .filter({ has: page.locator('.item_name', { hasText: '所属门店' }) }) + .locator('.item_val') + .click(); + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + // 判断某个门店被选中 + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + // 判断门店未被选中 + await expect( + page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' }), + ).not.toBeVisible(); + await page.locator('.label_checkbox', { hasText: 'AT测试一店' }).click(); + // 确认选中一店 + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page.locator('.ant-btn-primary', { hasText: '确定选择' }).first().click(); + // 搜索关键字 A + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.username[0]); + // 拥有会员卡A + await page + .locator('.item_sub') + .filter({ has: page.locator('.item_name', { hasText: '拥有的会员卡' }) }) + .locator('.item_val') + .click(); + await page.locator('.check_box_container', { hasText: '会员卡A' }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: '会员卡A' })).toBeVisible(); + await page.locator('.ant-btn-primary', { hasText: '确定选择' }).nth(5).click(); + // 点击搜索 + await page.locator('.ant-btn-block').click(); + await expect(page.locator('.m-table__fixed-left tbody tr', { hasText: customer.phone })).toBeVisible(); + }); + + await test.step('历史查询', async () => { + // 点击高级搜索旁边三个点 + await page.locator('.ant-btn-default .anticon').click(); + await page.locator('.ant-tabs-tab').first().waitFor(); + await page.locator('.ant-tabs-tab', { hasText: '历史搜索' }).click(); + await page.locator('.ant-tabs-tab-active', { hasText: '历史搜索' }).first().waitFor(); + + const $history = page + .locator('.search_list_ul .text', { hasText: `门店:AT测试一店;关键字${customer.username[0]};` }) + .first(); + await $history.click(); + + // 判断点击该搜索器是否能搜到该会员 + await expect(page.locator('.m-table__fixed-left tbody tr', { hasText: customer.phone })).toBeVisible(); + }); + }); + + test('快捷搜索', async ({ page, homeNavigation }) => { + // 进入顾客列表 + await homeNavigation.gotoModule('顾客'); + // 点击高级搜索旁边三个点 + await page.locator('.ant-btn-default .anticon').click(); + await page.locator('.ant-tabs-tab').first().waitFor(); + await page.locator('.ant-tabs-tab', { hasText: '快捷搜索' }).click(); + await page.locator('.ant-tabs-tab-active', { hasText: '快捷搜索' }).first().waitFor(); + await page + .locator('.class_tag') + .first() + .locator('.class_list', { hasText: /^会员$/ }) + .click(); + await expect(page.locator('.selected', { hasText: /^会员$/ }).first()).toBeVisible(); + await page + .locator('.ant-btn-lg', { hasText: /^搜\s索$/ }) + .last() + .click(); + + // 判断是否有会员标识 + const identification = page + .locator('.m-table__fixed-left .m-table__body tbody tr') + .first() + .locator('td .icon_style', { hasText: '会' }); + await expect(identification).toBeVisible(); + }); + }); + + test('顾客分页', async ({ page, homeNavigation }) => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 获取第一页会员信息 + await page.locator('.m-table-pagination .dec').waitFor(); + const member = page.locator('.m-table__fixed-left tbody tr'); + const pagePhone1 = await member.locator('td .user_info_body').first().innerText(); + // 点击第下一页 + await page.locator('.ant-pagination-next').click(); + // 获取第二页会员信息 + await page.locator('.m-table-pagination .dec').waitFor(); + const pagePhone2 = await member.locator('td .user_info_body').first().innerText(); + // 点击第下一页 + await page.locator('.ant-pagination-next').click(); + // 获取第三页会员信息 + await page.locator('.m-table-pagination .dec').waitFor(); + const pagePhone3 = await member.locator('td .user_info_body').first().innerText(); + expect(pagePhone1).not.toBe(pagePhone2); + expect(pagePhone1).not.toBe(pagePhone3); + expect(pagePhone2).not.toBe(pagePhone3); + }); +}); + +test.describe('顾客动态', () => { + test('新增事项', async ({ page, homeNavigation, createCustomer, customerPage }) => { + const customer = createCustomer; + + const today = new Date().getDate(); // 获取今日的天数 + const customerTr = page.locator('.m-table__body-wrapper tbody tr', { + hasText: customer.phone, + }); + const customerTd = customerTr.locator('td .cell'); + + await test.step('搜索顾客存在', async () => { + // 进入顾客页面 + await homeNavigation.gotoModule('顾客'); + // 跳转到顾客动态 + await customerPage.gotoSubPage('顾客动态'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await expect(page.locator('.m-table__fixed-left').getByText(customer.username)).toBeVisible(); + }); + + await test.step('切换时间为上月,新增事项', async () => { + await page.locator('.ant_date').getByPlaceholder('开始日期').click(); + await page.getByRole('button', { name: /^上\s月$/ }).click(); + + // 选择上月第一天 + await customerTd.first().click(); + + // 过去时间不可新增事项! + await expect(page.locator('.ant-message', { hasText: '过去时间不可新增事项!' })).toBeVisible(); + }); + + await test.step('切换时间为本月,新增事项', async () => { + await page.locator('.ant_date').getByPlaceholder('开始日期').click(); + await page.getByRole('button', { name: /^本\s月$/ }).click(); + + // 选择本月第today天(当天) + await customerTd.nth(today - 1).click(); + + await page.locator('.action .shopSelect_title', { hasText: '新增事项' }).waitFor(); + + await page.locator('label', { hasText: '电话回访' }).click(); + await page.getByPlaceholder('请输入1-100个字符的备注内容').fill('今日新增待办事项'); + + await page.getByRole('button', { name: '确认添加' }).click(); + await expect(customerTd.nth(today - 1).locator('.cell-warp—td')).toBeVisible(); + + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + + await test.step('查看今日事项,删除事项', async () => { + // 打开今日事项 + await customerTd.nth(today - 1).click(); + await expect(page.locator('.remark_area span')).toContainText('今日新增待办事项'); + await expect( + page + .locator('.item') + .filter({ has: page.locator('.name', { hasText: '事项状态' }) }) + .locator('.status'), + ).toContainText('已完成'); + + // 删除事项 + await page.locator('.action').getByRole('button', { name: '删除' }).click(); + await page + .locator('.popup_content') + .getByRole('button', { name: /^确\s认$/ }) + .click(); + await expect(customerTd.nth(today - 1).locator('.cell-warp—td')).not.toBeVisible(); + + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + await customerTd.nth(today - 1).click(); + await expect(page.locator('.action .shopSelect_title')).toContainText('新增事项'); + + // 关闭弹窗 + await page.locator('.action > div > div > .shopSelect_box > .shopSelect_heard > .close > svg').click(); + }); + + await test.step('新增明日事项,查看事项', async () => { + // 选择本月第today天的明天(明天) + await customerTd.nth(today).click(); + + await page.locator('.action .shopSelect_title', { hasText: '新增事项' }).waitFor(); + + await page.locator('label', { hasText: '电话回访' }).click(); + await page.getByPlaceholder('请输入1-100个字符的备注内容').fill('新增明日待办事项'); + await page.getByRole('button', { name: '确认添加' }).click(); + await expect(customerTd.nth(today).locator('.cell-warp—td')).toBeVisible(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + + // 打开明日事项 + await customerTd.nth(today).click(); + await page.locator('.action .shopSelect_title', { hasText: '电话回访' }).waitFor(); + await expect(page.locator('.remark_area span')).toContainText('新增明日待办事项'); + await expect( + page + .locator('.item') + .filter({ has: page.locator('.name', { hasText: '事项状态' }) }) + .locator('.status'), + ).toContainText('待完成'); + }); + }); + + test('完成事项', async ({ page, homeNavigation, createCustomer }) => { + const customer = createCustomer; + + // 事项状态 + let statusText; + // 当前顾客处于第几行,默认第一行 + let nowRow = 0; + // 获取当前月份 + const nowMonth = new Date().getMonth(); + // 获取今日的天数 + const today = new Date().getDate(); + + // 进入顾客动态 + await homeNavigation.gotoModule('顾客'); + await page.locator('.top_tab').getByText('顾客动态').click(); + await page.getByText('图标注释').waitFor(); + + // 根据手机号进行搜索 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix .search_btn').getByText('搜索', { exact: true }).click(); + await page.locator('.alertBox .close').waitFor(); + await page.locator('.custom_content', { hasText: customer.username }).click(); + + // 设置生日 + await page.locator('.m-table__fixed-left').getByText(customer.username).click(); + await page.locator('.person_info .user_name .edit_icon').click(); + await page.locator('.birth_content', { hasText: '日期' }).click(); + // 选择日 + await page + .getByRole('listbox') + .first() + .getByRole('option') + .nth(today - 1) + .click(); + // 选择月 + await page.getByRole('listbox').last().getByRole('option').nth(nowMonth).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + await page.getByRole('button', { name: /^保\s存$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '修改成功' })).toBeVisible(); + await page.locator('.close_icons svg').click(); + + // 切换时间为本月 + await page.locator('.ant_date').getByPlaceholder('开始日期').click(); + await page.getByRole('button', { name: /^本\s月$/ }).click(); + + // 选择本月第today天(当天) + const allTr = page.locator('.m-table__body-wrapper tbody tr'); + await allTr + .nth(nowRow) + .locator('td .cell') + .nth(today - 1) + .click(); + await page.getByRole('button', { name: '完成该事项' }).click(); + await page.getByPlaceholder('这里显示的回访完成后的反馈内容').fill('测试完成该事项'); + await page.getByRole('button', { name: /^提\s交$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + + const $statusText = page + .locator('.item') + .filter({ has: page.locator('.name', { hasText: '事项状态' }) }) + .locator('.status'); + await expect($statusText).toContainText('已完成'); + }); + + test('排序', async ({ page, homeNavigation, customerPage }) => { + const customerTrList = page.locator('.m-table-fixed-body tbody > tr'); + const consumeTotalList = page.locator('.consume_total'); + const amountTotalLocator = consumeTotalList.locator('.total_').filter({ hasText: '现金总额' }); + const lastTimeArrivalLocator = consumeTotalList.locator('.consume_list').filter({ hasText: '上次到店' }); + const closeMemberBoxLocator = page.locator('.close_icons'); + const sortToolLocator = page.locator('.table_tools').getByText('排序'); + let amountTotal, previousAmountTotal; + let lastTimeArrival, previousLastTimeArrival; + let tmp; + await test.step('上次到店时间', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客动态'); + await sortToolLocator.click(); + await page.getByRole('menuitem', { name: '上次到店时间从远到近' }).click(); + await customerTrList.nth(0).waitFor(); + await customerTrList.nth(0).locator('.name').click(); + tmp = await lastTimeArrivalLocator.locator('.detail_txt').innerText(); + lastTimeArrival = tmp.includes('--') ? 0 : Number(tmp); + previousLastTimeArrival = lastTimeArrival; + await closeMemberBoxLocator.click(); + + await sortToolLocator.click(); + await page.getByRole('menuitem', { name: '上次到店时间从近到远' }).click(); + await customerTrList.nth(0).waitFor(); + await customerTrList.nth(0).locator('.name').click(); + tmp = await lastTimeArrivalLocator.locator('.detail_txt').innerText(); + lastTimeArrival = tmp.includes('--') ? 0 : Number(tmp); + previousLastTimeArrival = lastTimeArrival; + await closeMemberBoxLocator.click(); + + expect(lastTimeArrival).toBeGreaterThanOrEqual(previousLastTimeArrival); + }); + + await test.step('现金总额', async () => { + await sortToolLocator.click(); + await page.getByRole('menuitem', { name: '现金总额从多到少' }).click(); + await customerTrList.nth(0).waitFor(); + await customerTrList.nth(0).locator('.name').click(); + tmp = await amountTotalLocator.locator('.total_num').innerText(); + amountTotal = Number(tmp); + previousAmountTotal = amountTotal; + await closeMemberBoxLocator.click(); + + await sortToolLocator.click(); + await page.getByRole('menuitem', { name: '现金总额从多到少' }).click(); + await customerTrList.nth(0).waitFor(); + await customerTrList.nth(0).locator('.name').click(); + tmp = await amountTotalLocator.locator('.total_num').innerText(); + amountTotal = Number(tmp); + previousAmountTotal = amountTotal; + await closeMemberBoxLocator.click(); + + expect(amountTotal).toBeLessThanOrEqual(previousAmountTotal); + }); + }); +}); + +test.describe('顾客详情', () => { + test('修改顾客资料(标签、档案、到店周期、备注)', async ({ + page, + homeNavigation, + customerPage, + createCustomer, + }) => { + const customer = createCustomer; + + const infoBox = page.locator('.info_box'); + + await test.step('设置顾客标签', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + const noSignLocator = infoBox.getByText('暂未添加标签'); + await expect(noSignLocator).toBeVisible(); + await infoBox.locator('.sign i').click(); + await expect(page.getByRole('button', { name: '移除所有选中' })).toBeVisible(); + const signSelectLocator = page.getByRole('treeitem', { name: '顾客六期管理' }); + await signSelectLocator.getByText('问题期').click(); + await signSelectLocator.getByText('铺垫期').click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(page.locator('.ant-message', { hasText: '修改成功' })).toBeVisible(); + await expect(noSignLocator).not.toBeVisible(); + await expect(page.locator('.sign_txt')).toContainText(['问题期', '铺垫期']); + }); + + await test.step('设置档案', async () => { + const infoBox = page.locator('.info_box'); + const popupLocator = page.locator('.popup_content'); + const titlePopup = popupLocator.locator('.title_box'); + const rightPopup = popupLocator.locator('.right_item_box'); + const archiveContent = faker.string.fromCharacters('编辑档案', 8); + const editArchive = page.locator('.box', { hasText: '编辑顾客档案' }); + const archivesLocator = infoBox.getByText('档案完成度'); + await expect(archivesLocator).toBeVisible(); + await infoBox.locator('.file_box .edit_icon').click(); + await expect(titlePopup.getByText('定制顾客档案', { exact: true })).toBeVisible(); + await rightPopup.locator('.edit_icon').click(); + await editArchive + .locator('.item', { hasText: '基本描述信息' }) + .getByPlaceholder('请输入1-500个字符备注内容') + .fill(archiveContent); + await editArchive.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + + // 打开档案编辑页面 + await expect(archivesLocator).toBeVisible(); + await infoBox.locator('.file_box .edit_icon').click(); + await expect(rightPopup).toContainText(archiveContent); + await page.locator('.title > .close_icon > svg > use').click(); + }); + + await test.step('修改到店周期', async () => { + const storeCycleDay = '0'; // 到店周期实际值 + const expectStoreCycleDay = '3'; // 到店周期期望值 + const consumeList = infoBox.locator('.consume_list'); + const storeCycle = consumeList.filter({ + has: page.getByText('到店周期', { exact: true }), + }); + + await storeCycle.waitFor(); + const storeCycleText = storeCycle.locator('.detail_txt'); + await expect(storeCycleText).toHaveText('0'); + + await storeCycle.locator('.edit_icon').click(); + await page.getByPlaceholder('请输入天数').fill(expectStoreCycleDay); + await page.locator('.number_box').getByRole('button').nth(11).click(); + await expect(page.locator('.ant-message', { hasText: '修改成功' })).toBeVisible(); + // 原来的值消失 + await expect(storeCycleText.filter({ hasText: storeCycleDay })).not.toBeVisible(); + // 更改为期望值显示 + await expect(storeCycleText.filter({ hasText: expectStoreCycleDay })).toBeVisible(); + }); + + await test.step('修改备注', async () => { + const popupLocator = page.locator('.popup_content'); + const remark = faker.string.fromCharacters('修改备注', 8); + const baseInfo = page.locator('.base_info .base_info_list'); + const remarkLocator = baseInfo.filter({ has: page.getByText('备注', { exact: true }) }); + await expect(async () => { + await remarkLocator.locator('.edit_icon').click(); + await popupLocator.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.locator('.ant-message', { hasText: '修改成功' })).toBeVisible(); + await expect(remarkLocator).toContainText(remark, { timeout: 3_000 }); + }).toPass(); + }); + }); + + test('在顾客详情页面开卡', async ({ page, homeNavigation, customerPage, createCustomer }) => { + const customer = createCustomer; + const card = page.locator('.has_card .membercard_box', { hasText: '原价卡' }); + + await test.step('开卡', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + // 去开卡 + await page + .locator('.info_package') + .getByRole('button', { name: /开\s卡/ }) + .click(); + await page.locator('.openCard_box', { hasText: '会员卡购买' }).waitFor(); + await page.locator('.openCard_box .memberCard_box', { hasText: '原价卡' }).click(); + + await page.locator('.remark', { hasText: '可输入卡备注' }).click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试顾客详情页面开卡'); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.getByRole('button', { name: '去结算' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + + await page.locator('.popup_content', { hasText: '会员协议签署确认' }).waitFor(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + + // 判断会员卡存在 + await expect(card.locator('.other_row .comment_item')).toHaveText('测试顾客详情页面开卡'); + }); + }); + + test('操作会员卡', async ({ page, homeNavigation, customerPage, createCustomer }) => { + const cardInfo = { name: '原价卡', gold: 5000, bonus: 3000 }; + + const card = page.locator('.has_card .membercard_box', { hasText: cardInfo.name }); + + const customer = createCustomer; + + await test.step('开卡', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.getByRole('button', { name: /开\s卡/ }).click(); + // 会员卡定位器 + const $card = page.locator('.openCard_box .memberCard_box', { hasText: cardInfo.name }); + await $card.click(); + // 判断卡金、赠金符合预期值 + await expect($card.locator('.balance.needsclick')).toContainText(`${cardInfo.gold}`); + await expect($card.locator('.bonus.needsclick')).toContainText(`${cardInfo.bonus}`); + // 备注会员卡,进行结算 + await page.locator('.remark', { hasText: '可输入卡备注' }).click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试操作会员卡'); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.getByRole('button', { name: '去结算' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + // 返回首页 + await page.locator('.anticon > svg').first().click(); + + // 进入顾客详情页面,判断会员卡存在 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + // 判断会员卡存在 + await expect(card.locator('.other_row .comment_item')).toHaveText('测试操作会员卡'); + }); + + const modifiedCardGold = cardInfo.gold + 1000; + await test.step('修改卡金', async () => { + await page.locator('.card_control_btn .more').click(); + await page.getByRole('menuitem', { name: '修改卡金' }).click(); + + await page.getByPlaceholder('请输入内容@.').fill(`${modifiedCardGold}`); + // 点击确认按钮 + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试修改卡金'); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + await expect(card.locator('.balances')).toContainText(`${modifiedCardGold}`); + }); + + const modifiedCardBonus = cardInfo.bonus + 1000; + await test.step('修改赠送金', async () => { + await page.locator('.card_control_btn .more').click(); + await page.getByRole('menuitem', { name: '修改赠送金' }).click(); + + await page.getByPlaceholder('请输入内容@.').fill(`${modifiedCardBonus}`); + // 点击确认按钮 + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试修改赠金'); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(card.locator('.bonuses')).toContainText(`${modifiedCardBonus}`); + }); + + // 随机卡号card**** + const modifiedCardNo = faker.helpers.fromRegExp(/card[0-9]{4}/); + await test.step('修改卡号', async () => { + await page.locator('.card_control_btn .more').click(); + await page.getByRole('menuitem', { name: '修改卡号' }).click(); + + await page.getByPlaceholder('请输入新卡号').fill(modifiedCardNo); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 判断卡号修改成功 + await expect(card.locator('.number_row .numebr')).toContainText(modifiedCardNo); + }); + + await test.step('修改有效期,会员卡左上角展示有效期提示“剩1天过期”', async () => { + await page.locator('.card_control_btn .more').click(); + await page.getByRole('menuitem', { name: '修改有效期' }).click(); + await page.getByPlaceholder('选择日期').click(); + await page + .locator('span') + .filter({ hasText: /^今天$/ }) + .click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 剩1天过期 + await expect(card.locator('.expireDay_box')).toContainText('剩1天'); + }); + + await test.step('删除会员卡', async () => { + await page.locator('.card_control_btn .more').click(); + await page.getByRole('menuitem', { name: '删除卡' }).click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试删除会员卡'); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(card).not.toBeVisible(); + await page.getByText('未使用').click(); + await page.locator('.ant-select-dropdown').getByRole('option', { name: '已删除' }).click(); + + await expect(card.locator('.card_name .overTime_tag').last()).toContainText('已删'); + }); + }); + + test('操作套餐', async ({ page, homeNavigation, customerPage, createCustomer }) => { + /**@type {string} */ + let billNo; + const date = new Date(); + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth() + 1; + const currentDay = date.getDate(); + const dayStr = currentDay >= 10 ? `${currentDay}` : `0${currentDay}`; + // 套餐的名称 + const $setMeal = page.getByText('护理修护全套'); + // 套餐的所有项目 + const $$treatCard = page.locator('.treat_card').filter({ has: $setMeal }); + + const customer = createCustomer; + + await test.step('开单买[护理修护全套]', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.getByText('搜索', { exact: true }).click(); + await page.locator('.member_list_li').filter({ hasText: customer.phone }).click(); + await page.getByText('套餐').click(); + await page.getByText('护理修护全套').last().click(); + await page.getByText(/结\s算/).click(); + await page.locator('.paymentInfoItem').first().click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await page.locator('.popup_content', { hasText: '会员协议签署确认' }).waitFor(); + + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /跳\s过/ }).click(), + ]); + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + expect(billNo).not.toBeNull(); + await page.getByRole('button', { name: '不寄存' }).click(); + }); + + await test.step('打开顾客详情页面', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + }); + + await test.step('查看套餐消耗记录', async () => { + await page.locator('label').filter({ hasText: '套餐' }).click(); + await expect($setMeal).toBeVisible(); + await $$treatCard.first().locator('svg').last().click(); + await page.getByText('消耗记录').click(); + await expect(page.getByText(billNo).last()).toBeVisible(); + await page + .locator('div') + .filter({ hasText: /^消耗记录$/ }) + .locator('use') + .click(); + }); + + await test.step('冻结有效期-解冻有效期', async () => { + // 冻结日期 + const freezeStr = `${currentYear}-${currentMonth}-${dayStr}冻结`; + // 冻结有效期 + await $$treatCard.first().locator('svg').last().click(); + await page.getByText('冻结有效期').click(); + await Promise.all([page.getByRole('button', { name: /确\s认/ }).click(), page.waitForLoadState()]); + await expect(page.locator('.ant-message')).toContainText('修改成功'); + await expect($$treatCard.first().locator('.deadline_row span')).toContainText(freezeStr); + // 解冻有效期 + await expect(async () => { + await $$treatCard.first().locator('svg').last().click(); + await page.getByText('解冻有效期').click({ timeout: 2000 }); + await expect($$treatCard.first().locator('.deadline_row span')).not.toContainText(freezeStr, { + timeout: 2000, + }); + }).toPass(); + }); + }); + + test('查看流水', async ({ page, homeNavigation, createCustomer, customerPage, numberInput }) => { + const remark = '测试查看流水' + faker.string.numeric(3); // 当前备注 + + const project = { no: '100018', name: '苹果精萃护理', shortName: '精萃护理', price: 980 }; + const goods = { no: 'aa100001', name: '家居搭配护理套', shortName: '', price: 3980 }; + const cardInfo = { name: '原价卡', gold: 5000, bonus: 3000 }; + // 预期的支付明细 + const paymentDetail = [ + { method: '卡金', amount: 1000 }, + { method: '赠金', amount: 1000 }, + { method: '积分', amount: 1000 }, + { method: '欠款', amount: 1000 }, + { method: '优惠券', amount: 10 }, + { method: '现金', amount: 950 }, + ]; + + const customer = createCustomer; + + await test.step('开卡', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + + await page.getByRole('button', { name: /开\s卡/ }).click(); + // 会员卡定位器 + const $card = page.locator('.openCard_box .memberCard_box', { hasText: cardInfo.name }); + await $card.click(); + // 判断卡金、赠金符合预期值 + await expect($card.locator('.balance.needsclick')).toContainText(`${cardInfo.gold}`); + await expect($card.locator('.bonus.needsclick')).toContainText(`${cardInfo.bonus}`); + + await page.getByRole('button', { name: '去结算' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + + // 判断会员卡存在 + const card = page.locator('.has_card .membercard_box', { hasText: cardInfo.name }); + await expect(card).toBeVisible(); + }); + + await test.step('赠送1000积分,1积分兑换1元', async () => { + await page.getByRole('tab', { name: '流水' }).click(); + await page.locator('.label_text', { hasText: '积分记录' }).click(); + await page.getByRole('button', { name: /^赠\s送$/ }).click(); + const point = paymentDetail.find(e => e.method === '积分')?.amount ?? 0; + await numberInput.setPointValue(point); + await numberInput.confirmValue(); + await page.locator('.popup_content', { hasText: '赠送积分备注' }).waitFor(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.locator('.popup_content input[type=radio]').click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + }); + + /**@type {string} 当前单号 */ + let billNo = ''; + + await test.step('购买项目并消耗,购买卖品并结算,结算使用优惠券、卡金、赠金、欠款、积分,拿取单号', async () => { + // 左右两侧的支付方式定位器 + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + + await page.locator('.right_util .goto_pay_cash', { hasText: '去开单' }).click(); + + // 购买项目并消耗,价格980 + await page.locator('.project_list .number', { hasText: project.no }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买卖品并结算,价格3980 + await page.locator('.float_tab .item', { hasText: '卖品' }).click(); + await page.locator('.project_list .number', { hasText: goods.no }).click(); + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + + // 赠送优惠券,价格10 + await page.locator('.payMain .left .body .row', { hasText: '优惠券抵扣' }).click(); + await page.getByRole('button', { name: '赠送优惠券' }).click(); + await page.locator('.popup_content', { hasText: '选择优惠券' }).waitFor(); + await page + .locator('.popup_content .list .item', { hasText: '定额10元券' }) + .locator('input[type="checkbox"]') + .click(); + await page + .locator('.popup_content') + .getByRole('button', { name: /^确\s定$/ }) + .click(); + + // 使用优惠券 + await page.locator('.m_sliding_menu .body .box').first().locator('input[type="checkbox"]').check(); + await page.getByRole('button', { name: '确认选择' }).click(); + + await leftPaymentInfoItem.filter({ hasText: '混合支付' }).click(); + + await rightPaymentInfoItem.filter({ hasText: '卡金' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await numberInput.setCommonValue(paymentDetail.find(e => e.method === '卡金')?.amount ?? 0); + await numberInput.confirmValue(); + + await rightPaymentInfoItem.filter({ hasText: '赠金' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await numberInput.setCommonValue(paymentDetail.find(e => e.method === '赠金')?.amount ?? 0); + await numberInput.confirmValue(); + + await rightPaymentInfoItem.filter({ hasText: '欠款' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await numberInput.setCommonValue(paymentDetail.find(e => e.method === '欠款')?.amount ?? 0); + await numberInput.confirmValue(); + + await rightPaymentInfoItem.filter({ hasText: '积分' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await numberInput.setCommonValue(paymentDetail.find(e => e.method === '积分')?.amount ?? 0); + await numberInput.confirmValue(); + + await rightPaymentInfoItem.filter({ hasText: '现金' }).first().click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await numberInput.confirmValue(); + + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /^结\s算$/ }).click(), + ]); + + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + expect(billNo).not.toBeNull(); + await page.getByRole('button', { name: '不寄存' }).click(); + }); + + const consumeTrLocator = page.locator('.consume_table .m-table__body tr'); + const billNoLocator = consumeTrLocator.locator('.bill_no', { hasText: billNo }); + const billTrLocator = consumeTrLocator.filter({ + has: page.locator('.bill_no', { hasText: billNo }), + }); + + await test.step('进入顾客的详情页面的流水', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + await page.getByRole('tab', { name: '流水' }).click(); + }); + + // 消费记录区域 + const consume = page.locator('.m-detailComponent_warp'); + // 购买区 + const consumeBuy = consume.locator('.m-detailComponent_item').nth(0); + // 支付明细 + const consumeDetail = consume.locator('.m-detailComponent_consume'); + + await test.step('查看购买记录和单据明细', async () => { + // 购买记录 + await page.locator('.label_text', { hasText: '购买记录' }).click(); + + await expect(billNoLocator).toBeVisible(); + await billNoLocator.click(); + + await consumeBuy.locator('.main-table-body_tr').last().waitFor(); + + // 项目简称、卖品名称 + await expect(consumeBuy.locator('.main-table-body_tr')).toContainText([project.name, goods.name]); + + // 总金额 + await expect(consumeDetail.locator('.amount')).toContainText(`${project.price + goods.price}`); + + // 欠款金额 + await expect(consumeDetail.locator('.overdraft')).toContainText( + `${paymentDetail.find(e => e.method === '欠款')?.amount}`, + ); + + await expect(consumeDetail.locator('.m-detailComponent_consume_detail > div')).toContainText([ + `优惠券¥${paymentDetail.find(e => e.method === '优惠券')?.amount}`, + `卡金¥${paymentDetail.find(e => e.method === '卡金')?.amount}`, + `赠金¥${paymentDetail.find(e => e.method === '赠金')?.amount}`, + `积分¥${paymentDetail.find(e => e.method === '积分')?.amount}`, + `现金¥${paymentDetail.find(e => e.method === '现金')?.amount}`, + ]); + + await page + .locator('div') + .filter({ hasText: /^单据明细$/ }) + .locator('i') + .click(); + }); + + await test.step('查看赠送记录', async () => { + await page.locator('.label_text', { hasText: '赠送记录' }).click(); + + await expect(billNoLocator).not.toBeVisible(); + }); + + await test.step('查看消耗记录', async () => { + // 消耗记录 + await page.locator('.label_text', { hasText: '消耗记录' }).click(); + // 单据明细 + await expect.soft(billNoLocator).toBeVisible(); + // 项目简称 + await expect.soft(billTrLocator.first().locator('.consume_shortname')).toContainText(project.shortName); + await expect.soft(billTrLocator.first().locator('.num')).toContainText('1'); + await expect(billTrLocator.first().locator('.price')).toContainText(`${project.price}`); + }); + + await test.step('查看退换记录', async () => { + // 退换记录 + await page.locator('.label_text', { hasText: '退换记录' }).click(); + + await expect(billNoLocator).not.toBeVisible(); + }); + + await test.step('查看会员卡记录', async () => { + await page.locator('.label_text', { hasText: '会员卡记录' }).click(); + await page.getByRole('cell', { name: '单号' }).waitFor(); + const row = consumeTrLocator + .filter({ + has: page.locator('td', { hasText: '消费' }), + }) + .filter({ + has: page.locator('td', { hasText: billNo }), + }); + // 卡金 + await expect(row.locator('td').nth(4)).toContainText( + `${paymentDetail.find(e => e.method === '卡金')?.amount}`, + ); + // 赠金 + await expect(row.locator('td').nth(5)).toContainText( + `${paymentDetail.find(e => e.method === '赠金')?.amount}`, + ); + }); + + await test.step('查看积分记录', async () => { + await page.locator('.label_text', { hasText: '积分记录' }).click(); + await expect( + consumeTrLocator + .filter({ has: page.locator('td', { hasText: '线下抵扣' }) }) + .locator('td') + .nth(1), + ).toContainText(`${paymentDetail.find(e => e.method === '积分')?.amount}`); + }); + + await test.step('查看优惠券记录', async () => { + await page.locator('.label_text', { hasText: '优惠券记录' }).click(); + + const pointLocator = consumeTrLocator + .filter({ has: page.locator('td', { hasText: '定额10元券' }) }) + .filter({ has: page.locator('td', { hasText: '已使用' }) }) + .filter({ has: page.locator('td', { hasText: /¥\s4960/ }) }); + + await expect(pointLocator).toBeVisible(); + }); + + await test.step('查看卖品记录', async () => { + await page.locator('.label_text', { hasText: '卖品记录' }).click(); + + const productLocator = consumeTrLocator + .filter({ has: page.locator('td', { hasText: billNo }) }) + .filter({ has: page.locator('td', { hasText: '购买' }) }) + .filter({ has: page.locator('td', { hasText: goods.name }) }); + + await expect(productLocator).toBeVisible(); + }); + + await test.step('查看欠还款记录,并且还款', async () => { + await page.locator('.label_text', { hasText: '欠还款记录' }).click(); + + const paybackLocator = page + .locator('.reimbursement_list') + .filter({ + has: page.locator('.reimbursement_order', { hasText: billNo }), + }) + .first(); + + // 还款存在 + await expect(paybackLocator).toBeVisible(); + + // 进行还款 + await paybackLocator.getByRole('button', { name: /^还\s款$/ }).click(); + await page.getByText('现金', { exact: true }).click(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(paybackLocator.filter({ hasText: '已清' })).toBeVisible(); + }); + }); + + test('查看动态', async ({ page, homeNavigation, createCustomer, customerPage, customerDetailsPage }) => { + const customer = createCustomer; + const date = new Date(); + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth() + 1; + const currentDay = date.getDate(); + + /**@type {string} */ + let billNo = ''; + await test.step('开单拿取单号', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.getByText('搜索', { exact: true }).click(); + await page.locator('.member_list_li').filter({ hasText: customer.phone }).click(); + await page.locator('.list_box .project_list').first().click(); + await page + .locator('.pay_btn') + .filter({ hasText: /^结\s算$/ }) + .click(); + //取消推送消息提醒 + await page.getByLabel('推送消费提醒').uncheck(); + //取消结算签字 + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paymentInfoItem').first().click(); + + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /^结\s算$/ }).click(), + ]); + + billNo = (await response.json())?.content?.billNo; + expect(billNo).not.toBeNull(); + }); + + await test.step('进入顾客详情页面', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + }); + + await test.step('查看日动态', async () => { + // 日历列表 + const $$calendarList = page.locator('.calendar_list').filter({ + has: page.getByText(`${currentYear}年${currentMonth.toString().padStart(2, '0')}月`, { + exact: true, + }), + }); + // 当前日期的动态 + const $$dayForDynamics = $$calendarList + .getByTitle(`${currentMonth}月${currentDay}日`) + .getByRole('listitem') + .locator('div'); + // 进入顾客详情动态页面 + await customerDetailsPage.gotoSubPage('动态'); + await expect($$dayForDynamics.first()).toBeInViewport(); + await $$dayForDynamics.first().click(); + await page.getByRole('button', { name: '查看消费详情' }).click(); + await expect(page.getByText(billNo)).toBeVisible(); + await page + .locator('div') + .filter({ hasText: /^单据明细$/ }) + .locator('svg') + .click(); + await page.locator('.shopSelect_heard > .close > svg').first().click(); + }); + + // 今年所有月份的动态 + const $$dynamic = page.locator('.calendar_year_list', { hasText: `${currentYear}` }).locator('.dynamic_list'); + // 当前月份的动态 + const $currentForMonthDynamic = $$dynamic.filter({ + has: page.locator('.month_box', { hasText: `${currentMonth}` }), + }); + + await test.step('查看月动态-缩略', async () => { + await page + .locator('div') + .filter({ hasText: /^日月$/ }) + .getByRole('switch') + .click(); + await expect(page.getByText('月', { exact: true })).toHaveClass('selected_btn'); + + await expect(async () => { + await $currentForMonthDynamic.click(); + await expect(page.getByRole('tabpanel').getByText(billNo)).toBeVisible(); + }).toPass(); + await page.locator('.action_detail > .container > .m_sliding_menu > .box > .top > .anticon').click(); + }); + + await test.step('查看月动态-日期', async () => { + await page + .locator('div') + .filter({ hasText: /^缩略日期$/ }) + .getByRole('switch') + .click(); + await expect(page.getByText('日期', { exact: true })).toHaveClass('selected_btn'); + + await $currentForMonthDynamic.filter({ hasText: `${currentDay}` }).click(); + await expect(page.getByRole('tabpanel').getByText(billNo)).toBeVisible(); + }); + }); + + test('日志-添加-修改-删除', async ({ page, homeNavigation, customerPage, createCustomer }) => { + const customer = createCustomer; + let logContent = '测试添加日志成功'; + let lastLogContent = ''; + // 日志列表 + const $$journal = page.locator('.journal_list'); + // 日志填写框 + const $input = page.getByPlaceholder('请输入1-500个字符护理内容'); + + await test.step('进入顾客详情页面', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + }); + + await test.step('添加日志', async () => { + await page.getByRole('tab', { name: '日志' }).click(); + await page.getByRole('button', { name: '添加日志' }).click(); + await $input.fill(logContent); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 在顾客A的顾客详情页面 -》日志查看刚才添加的日志 + await expect($$journal.first()).toContainText(logContent); + }); + + lastLogContent = logContent; + logContent = '修改日志'; + await test.step('修改日志', async () => { + await $$journal.first().locator('.title a').click(); + await page.getByRole('menuitem', { name: '编辑' }).click(); + + await $input.waitFor(); + const currentLogContent = await $input.inputValue(); + expect(currentLogContent).toBe(lastLogContent); + + await $input.fill(logContent); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect($$journal.first()).toContainText(logContent); + }); + + await test.step('删除日志', async () => { + await expect($$journal.first()).toContainText(logContent); + await $$journal.first().locator('.title a').click(); + await page.getByRole('menuitem', { name: '删除' }).click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect($$journal.first()).not.toBeVisible(); + }); + }); +}); + +test.describe('顾客概要', () => { + test('顾客列表展示', async ({ page, homeNavigation }) => { + // 定义会员数据 + let TotalCash2; + let TotalCash3; + let ConsumptionFrequency2; + let ConsumptionFrequency3; + let LastArrivalTime2; + let LastArrivalTime3; + let LastConsumptionTime2; + let LastConsumptionTime3; + let FirstConsumptionTime2; + let FirstConsumptionTime3; + let RegistrationTime2; + let RegistrationTime3; + + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击会员列表下拉框 + await page.locator('.sub_icon').first().click(); + // 现金总额 + await page.locator('.m-table-cell-sort').filter({ hasText: '现金总额' }).click(); + // 点击最后一页 + const endPage = await page.locator('.m-table-pagination li').count(); + await page + .locator('.m-table-pagination li') + .nth(endPage - 3) + .click(); + // 顾客数量 + const memberquantity = await page.locator('.main-table-body_tr').count(); + console.log('最后一页' + memberquantity); + + await test.step('倒数三个对比正数三个的现金总额', async () => { + const $$cash = page.locator('.main-table-body_tr .is-right:nth-child(12)'); + const TotalCash1 = await $$cash.nth(-1).innerText(); + if (memberquantity === 1) { + await page.locator('.ant-pagination-item-link').first().click(); + TotalCash2 = await $$cash.nth(-1).innerText(); + TotalCash3 = await $$cash.nth(-2).innerText(); + } + + if (memberquantity === 2) { + TotalCash2 = await $$cash.nth(-2).innerText(); + await page.locator('.ant-pagination-item-link').first().click(); + TotalCash3 = await $$cash.nth(-1).innerText(); + } + + if (memberquantity > 2) { + TotalCash2 = await $$cash.nth(-2).innerText(); + TotalCash3 = await $$cash.nth(-3).innerText(); + } + + // 点击现金总额排序 + await page.locator('.m-table-cell-sort').filter({ hasText: '现金总额' }).click(); + await expect($$cash.nth(0)).toContainText(TotalCash1); + await expect($$cash.nth(1)).toContainText(TotalCash2); + await expect($$cash.nth(2)).toContainText(TotalCash3); + }); + + await test.step('倒数三个对比正数三个的消费次数', async () => { + await page.locator('.m-table-cell-sort').filter({ hasText: '消费次数' }).click(); + const $consumptionFrequency = page.locator('.main-table-body_tr .is-right:nth-child(13)'); + // 点击最后一页 + await page + .locator('.m-table-pagination li') + .nth(endPage - 3) + .click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + const ConsumptionFrequency1 = await $consumptionFrequency.nth(-1).innerText(); + if (memberquantity === 1) { + await page.locator('.ant-pagination-item-link').first().click(); + ConsumptionFrequency2 = await $consumptionFrequency.nth(-1).innerText(); + ConsumptionFrequency3 = await $consumptionFrequency.nth(-2).innerText(); + } + + if (memberquantity === 2) { + ConsumptionFrequency2 = await $consumptionFrequency.nth(-2).innerText(); + await page.locator('.ant-pagination-item-link').first().click(); + ConsumptionFrequency3 = await $consumptionFrequency.nth(-1).innerText(); + } + + if (memberquantity > 2) { + ConsumptionFrequency2 = await page + .locator('.main-table-body_tr .is-right:nth-child(13)') + .nth(-2) + .innerText(); + ConsumptionFrequency3 = await page + .locator('.main-table-body_tr .is-right:nth-child(13)') + .nth(-3) + .innerText(); + } + await page.locator('.m-table-cell-sort').filter({ hasText: '消费次数' }).click(); + await expect(page.locator('.main-table-body_tr .is-right:nth-child(13)').nth(0)).toBeVisible(); + + await expect($consumptionFrequency.nth(0)).toContainText(ConsumptionFrequency1); + await expect($consumptionFrequency.nth(1)).toContainText(ConsumptionFrequency2); + await expect($consumptionFrequency.nth(2)).toContainText(ConsumptionFrequency3); + }); + + await test.step('倒数三个对比正数三个的上次到店时间', async () => { + await page.locator('.m-table-cell-sort').filter({ hasText: '上次到店时间' }).click(); + // 点击最后一页 + await page + .locator('.m-table-pagination li') + .nth(endPage - 3) + .click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + const $$lastArrivalTime = page.locator('.main-table-body_tr .is-center:nth-child(15)'); + + const LastArrivalTime1 = await $$lastArrivalTime.nth(-1).innerText(); + if (memberquantity === 1) { + await page.locator('.ant-pagination-item-link').first().click(); + LastArrivalTime2 = await $$lastArrivalTime.nth(-1).innerText(); + LastArrivalTime3 = await $$lastArrivalTime.nth(-2).innerText(); + } + + if (memberquantity === 2) { + LastArrivalTime2 = await $$lastArrivalTime.nth(-2).innerText(); + await page.locator('.ant-pagination-item-link').first().click(); + LastArrivalTime3 = await $$lastArrivalTime.nth(-1).innerText(); + } + + if (memberquantity > 2) { + LastArrivalTime2 = await $$lastArrivalTime.nth(-2).innerText(); + LastArrivalTime3 = await $$lastArrivalTime.nth(-3).innerText(); + } + + await page.locator('.m-table-cell-sort').filter({ hasText: '上次到店时间' }).click(); + await expect($$lastArrivalTime.nth(0)).toContainText(LastArrivalTime1); + await expect($$lastArrivalTime.nth(1)).toContainText(LastArrivalTime2); + await expect($$lastArrivalTime.nth(2)).toContainText(LastArrivalTime3); + }); + + await test.step('倒数三个对比正数三个的上次消费时间', async () => { + await page.locator('.m-table-cell-sort').filter({ hasText: '上次消费时间' }).click(); + // 点击最后一页 + await page + .locator('.m-table-pagination li') + .nth(endPage - 3) + .click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + // 上次消费时间定位器 + const $$lastConsumptionTime = page.locator('.main-table-body_tr .is-center:nth-child(16)'); + + const LastConsumptionTime1 = await $$lastConsumptionTime.nth(-1).innerText(); + if (memberquantity === 1) { + await page.locator('.ant-pagination-item-link').first().click(); + LastConsumptionTime2 = await $$lastConsumptionTime.nth(-1).innerText(); + LastConsumptionTime3 = await $$lastConsumptionTime.nth(-2).innerText(); + } + if (memberquantity === 2) { + LastConsumptionTime2 = await $$lastConsumptionTime.nth(-2).innerText(); + await page.locator('.ant-pagination-item-link').first().click(); + LastConsumptionTime3 = await $$lastConsumptionTime.nth(-1).innerText(); + } + if (memberquantity > 2) { + LastConsumptionTime2 = await $$lastConsumptionTime.nth(-2).innerText(); + LastConsumptionTime3 = await $$lastConsumptionTime.nth(-3).innerText(); + } + + await page.locator('.m-table-cell-sort').filter({ hasText: '上次消费时间' }).click(); + await expect($$lastConsumptionTime.nth(0)).toContainText(LastConsumptionTime1); + await expect($$lastConsumptionTime.nth(1)).toContainText(LastConsumptionTime2); + await expect($$lastConsumptionTime.nth(2)).toContainText(LastConsumptionTime3); + }); + + await test.step('倒数三个对比正数三个的首次消费时间', async () => { + await page.locator('.m-table-cell-sort').filter({ hasText: '首次消费时间' }).click(); + // 点击最后一页 + await page + .locator('.m-table-pagination li') + .nth(endPage - 3) + .click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + // 首次消费时间定位器 + const $$firstConsumptionTime = page.locator('.main-table-body_tr .is-center:nth-child(17)'); + + const FirstConsumptionTime1 = await $$firstConsumptionTime.nth(-1).innerText(); + if (memberquantity === 1) { + await page.locator('.ant-pagination-item-link').first().click(); + FirstConsumptionTime2 = await $$firstConsumptionTime.nth(-1).innerText(); + FirstConsumptionTime3 = await $$firstConsumptionTime.nth(-2).innerText(); + } + if (memberquantity === 2) { + FirstConsumptionTime2 = await $$firstConsumptionTime.nth(-2).innerText(); + await page.locator('.ant-pagination-item-link').first().click(); + FirstConsumptionTime3 = await $$firstConsumptionTime.nth(-1).innerText(); + } + if (memberquantity > 2) { + FirstConsumptionTime2 = await $$firstConsumptionTime.nth(-2).innerText(); + FirstConsumptionTime3 = await $$firstConsumptionTime.nth(-3).innerText(); + } + + await page.locator('.m-table-cell-sort').filter({ hasText: '首次消费时间' }).click(); + await expect($$firstConsumptionTime.nth(0)).toContainText(FirstConsumptionTime1); + await expect($$firstConsumptionTime.nth(1)).toContainText(FirstConsumptionTime2); + await expect($$firstConsumptionTime.nth(2)).toContainText(FirstConsumptionTime3); + }); + + await test.step('倒数三个对比正数三个的注册时间', async () => { + // 注册时间 + await page.locator('.m-table-cell-sort').filter({ hasText: '注册时间' }).click(); + // 点击最后一页 + await page + .locator('.m-table-pagination li') + .nth(endPage - 3) + .click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + // 注册时间定位器 + const $$registrationTime = page.locator('.main-table-body_tr .is-center:nth-child(18)'); + + const RegistrationTime1 = await $$registrationTime.nth(-1).innerText(); + if (memberquantity === 1) { + await page.locator('.ant-pagination-item-link').first().click(); + RegistrationTime2 = await $$registrationTime.nth(-1).innerText(); + RegistrationTime3 = await $$registrationTime.nth(-2).innerText(); + } + if (memberquantity === 2) { + RegistrationTime2 = await $$registrationTime.nth(-2).innerText(); + await page.locator('.ant-pagination-item-link').first().click(); + RegistrationTime3 = await $$registrationTime.nth(-1).innerText(); + } + if (memberquantity > 2) { + RegistrationTime2 = await $$registrationTime.nth(-2).innerText(); + RegistrationTime3 = await $$registrationTime.nth(-3).innerText(); + } + await page.locator('.m-table-cell-sort').filter({ hasText: '注册时间' }).click(); + await expect($$registrationTime.nth(0)).toContainText(RegistrationTime1); + await expect($$registrationTime.nth(1)).toContainText(RegistrationTime2); + await expect($$registrationTime.nth(2)).toContainText(RegistrationTime3); + }); + }); + + test('待办展示', async ({ page, homeNavigation, createCustomCustomer }) => { + const date = new Date(); + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth() + 1; + const currentDay = date.getDate(); + const customer = new Customer(1, 1, { + birthday: { year: currentYear, month: currentMonth, day: currentDay }, + }); + const todoLocator = page.locator('.member-side', { hasText: '待办事项' }); + const todoListLocator = todoLocator.locator('.action_item'); + const customerTodo = todoListLocator.filter({ has: page.getByText(customer.username) }); + const showTodoLocator = page.locator('.shopSelect_box', { hasText: '事项状态' }); + + // 创建顾客,并且设置了顾客生日为今日 + await createCustomCustomer(customer); + // 进入顾客模块 + await homeNavigation.gotoModule('顾客'); + // 待办列表存在 + await expect(todoLocator).toBeVisible(); + // 打开顾客的生日待办事项 + await customerTodo.getByText('生日提醒', { exact: true }).click(); + // 判断待办事项正确展示 + await expect(showTodoLocator).toContainText('生日提醒'); + await expect(showTodoLocator).toContainText(customer.username); + }); + + test.skip('表显示配置', async ({ page, homeNavigation }) => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 点击更多 + await page.locator('.more', { hasText: '更多' }).waitFor(); + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + // 关闭档案号、项目的显示开关 + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^档案号$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchA = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^档案号$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已关闭 + await expect(SwitchA).not.toBeVisible(); + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^项目$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchB = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^项目$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已关闭 + await expect(SwitchB).not.toBeVisible(); + // 确认 + await page.locator('.ant-btn-primary', { hasText: /^确\s认$/ }).click(); + + // 遍历标题 项目 档案号不存在 + const allTrB = page.locator('.m-table__header-wrapper thead th'); + const archivesB = allTrB.locator('.m-table-cell', { hasText: /^档案号$/ }); + const ProjectB = allTrB.locator('.m-table-cell', { hasText: /^项目$/ }); + await expect(archivesB).not.toBeVisible(); + await expect(ProjectB).not.toBeVisible(); + + // 点击更多 + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + // 关闭档案号、项目的显示开关 + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^档案号$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchC = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^档案号$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已打开 + await expect(SwitchC).toBeVisible(); + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^项目$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchD = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^项目$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已打开 + await expect(SwitchD).toBeVisible(); + // 确认 + await page.locator('.ant-btn-primary', { hasText: /^确\s认$/ }).click(); + + // 遍历标题 项目 档案号存在 + const allTrA = page.locator('.m-table__header-wrapper thead th'); + const archivesA = allTrA.locator('.m-table-cell', { hasText: /^档案号$/ }); + const ProjectA = allTrA.locator('.m-table-cell', { hasText: /^项目$/ }); + await expect(archivesA).toBeVisible(); + await expect(ProjectA).toBeVisible(); + + // 点击更多 + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + const sortArchives = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^档案号$/ }) }) + .locator('.sort'); + const sortItem = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^项目$/ }) }) + .locator('.sort'); + + // 边界框 + const boundaryArchives = await sortArchives.boundingBox(); + const boundaryItem = await sortItem.boundingBox(); + + if (!boundaryArchives || !boundaryItem) { + throw new Error('获取边界框失败'); + } + + const ArchivesX = boundaryArchives.x + boundaryArchives.width / 2; + const ArchivesY = boundaryArchives.y + boundaryArchives.height / 2; + + const ItemX = boundaryItem.x + boundaryItem.width / 2; + const ItemY = boundaryItem.y + boundaryItem.height / 2; + + await expect(async () => { + // 拖动项目到档案号的位置 + await page.mouse.move(ItemX, ItemY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(ArchivesX, ArchivesY); + await page.mouse.up(); + expect(await page.locator('.sortList .list-session .name').first().innerText()).toBe('项目'); + }).toPass(); + + await expect(async () => { + // 拖动项目到档案号的位置 + await page.mouse.move(ItemX, ItemY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(ArchivesX, ArchivesY); + await page.mouse.up(); + expect(await page.locator('.sortList .list-session .name').first().innerText()).toBe('档案号'); + }).toPass(); + }); + + test.skip('导出表格', async ({ page, homeNavigation }) => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 点击更多 + await page.locator('.more', { hasText: '更多' }).waitFor(); + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + const downloadPromise = page.waitForEvent('download'); + // 点击导出数据 + await page.locator('.a-modal-export', { hasText: '导出数据' }).click(); + // 等待报表导出 + await page.locator('.m-exportModal_title', { hasText: '报表导出' }).waitFor(); + // 确认 + await page.locator('.m-exportModal_button', { hasText: /^确\s认$/ }).click(); + const download = await downloadPromise; + // 获取下载的文件名 + const fileName = download.suggestedFilename(); + // 指定保存下载文件的路径 + const downloadPath = path.join(__dirname, `@/imgs/${fileName}`); + // 保存下载的文件 + await download.saveAs(downloadPath); + // 验证文件是否成功保存 + const fileExists = fs.existsSync(downloadPath); + expect(fileExists).toBeTruthy(); + }); +}); + +test.describe('顾客分配', () => { + test('顾客列表展示', async ({ page, homeNavigation, customerPage }) => { + await test.step('查看总人数和页脚总人数一致', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客分配'); + const headTotalLocator = page.locator('.info_item', { hasText: '总人数' }); + await waitStable(headTotalLocator); + const paginationLocator = page.locator('.m-table-pagination .dec'); + await paginationLocator.waitFor(); + const paginationMatch = (await paginationLocator.innerText()).match(/共计([0-9]+)/); + const paginationTotal = paginationMatch ? paginationMatch[1] : '0'; + const headTotal = await headTotalLocator.locator('.info_number').innerText(); + expect.soft(headTotal).toBe(paginationTotal); + }); + + await test.step('按照现金总额排序查看列表', async () => { + const amountTotalLocator = page.locator('.m-table__header-wrapper th', { + hasText: '现金总额', + }); + + // 顺序和倒序的按钮 + const amountTotalOrder = amountTotalLocator.locator('.m-table-sort i').last(); + const amountTotalReverseOrder = amountTotalLocator.locator('.m-table-sort i').first(); + const amountList = page.locator('.m-table__body-wrapper .main-table-body_tr'); + + // 查看从高到底的排序 + await amountTotalOrder.click(); + await expect(amountTotalOrder).toHaveClass(/on/); + await amountList.first().waitFor(); + + // 拿取现金总额列的index + const titleListLocator = page.locator('.m-table__header-wrapper tr').first().locator('th'); + const titleList = await titleListLocator.allInnerTexts(); + const index = titleList.findIndex(text => text === '现金总额'); + + // 拿取前三个顾客的现金总额 + let amountArray = []; + for (let i = 0; i < 3; i++) { + const amountStr = await amountList.nth(i).locator('td').nth(index).innerText(); + const amount = convertAmountText(amountStr).amount; + amountArray.push(amount); + } + + // 金额按照从高到低排列 + expect(amountArray[0]).toBeGreaterThanOrEqual(amountArray[1]); + expect(amountArray[1]).toBeGreaterThanOrEqual(amountArray[2]); + + // 查看从低到高的排序 + await amountTotalReverseOrder.click(); + await expect(amountTotalOrder).toHaveClass(/on/); + await amountList.first().waitFor(); + + amountArray = []; + let amountStr = await amountList.nth(0).locator('td').nth(index).innerText(); + amountArray.push(convertAmountText(amountStr).amount); + + // 跳转到倒数第一页,拿取最后三个顾客的现金总额 + const paginationLiLocator = page.locator('.m-table-pagination ul li'); + const paginationLiCount = await paginationLiLocator.count(); + await paginationLiLocator.nth(paginationLiCount - 3).click(); + amountStr = await amountList.nth(0).locator('td').nth(index).innerText(); + amountArray.push(convertAmountText(amountStr).amount); + // 金额按照从高到低排列 + expect(amountArray[0]).toBeLessThanOrEqual(amountArray[1]); + }); + }); + + test.skip('表显示配置', async ({ page, homeNavigation }) => { + await page.reload(); // 跟分配师一样不重进不加载某些内容 + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击顾客分配 + await page.locator('.tab_item', { hasText: '顾客分配' }).first().click(); + await page.locator('.assign_info').waitFor(); + // 点击更多 + await page.locator('.more', { hasText: '更多' }).waitFor(); + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + // 关闭档案号、项目的显示开关 + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^咨询师$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchA = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^咨询师$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已关闭 + await expect(SwitchA).not.toBeVisible(); + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^医生$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchB = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^医生$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已关闭 + await expect(SwitchB).not.toBeVisible(); + // 确认 + await page.locator('.ant-btn-primary', { hasText: /^确\s认$/ }).click(); + + // 遍历标题 项目 档案号不存在 + const allTrB = page.locator('.m-table__header-wrapper thead th'); + const archivesB = allTrB.locator('.m-table-cell', { hasText: /^咨询师$/ }); + const ProjectB = allTrB.locator('.m-table-cell', { hasText: /^医生$/ }); + await expect(archivesB).not.toBeVisible(); + await expect(ProjectB).not.toBeVisible(); + + // 点击更多 + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + // 关闭档案号、项目的显示开关 + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^咨询师$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchC = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^咨询师$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已打开 + await expect(SwitchC).toBeVisible(); + await page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^医生$/ }) }) + .locator('.ant-switch') + .click(); + const SwitchD = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^医生$/ }) }) + .locator('.ant-switch-checked'); + // 判断开关已打开 + await expect(SwitchD).toBeVisible(); + // 确认 + await page.locator('.ant-btn-primary', { hasText: /^确\s认$/ }).click(); + + // 遍历标题 项目 档案号存在 + const allTrA = page.locator('.m-table__header-wrapper thead th'); + const archivesA = allTrA.locator('.m-table-cell', { hasText: /^咨询师$/ }); + const ProjectA = allTrA.locator('.m-table-cell', { hasText: /^医生$/ }); + await expect(archivesA).toBeVisible(); + await expect(ProjectA).toBeVisible(); + + // 点击更多 + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + const sortArchives = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^咨询师$/ }) }) + .locator('.sort'); + const sortItem = page + .locator('.sortList .list-session') + .filter({ has: page.locator('.name', { hasText: /^医生$/ }) }) + .locator('.sort'); + + // 边界框 + const boundaryArchives = await sortArchives.boundingBox(); + const boundaryItem = await sortItem.boundingBox(); + + if (!boundaryArchives || !boundaryItem) { + throw new Error('获取边界框失败'); + } + + const ArchivesX = boundaryArchives.x + boundaryArchives.width / 2; + const ArchivesY = boundaryArchives.y + boundaryArchives.height / 2; + + const ItemX = boundaryItem.x + boundaryItem.width / 2; + const ItemY = boundaryItem.y + boundaryItem.height / 2; + + await expect(async () => { + // 拖动项目到档案号的位置 + await page.mouse.move(ItemX, ItemY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(ArchivesX, ArchivesY); + await page.mouse.up(); + expect(await page.locator('.sortList .list-session .name').first().innerText()).toBe('医生'); + }).toPass(); + + await expect(async () => { + // 拖动项目到档案号的位置 + await page.mouse.move(ItemX, ItemY); + await page.mouse.down(); + await page.waitForTimeout(2000); + await page.mouse.move(ArchivesX, ArchivesY); + await page.mouse.up(); + expect(await page.locator('.sortList .list-session .name').first().innerText()).toBe('咨询师'); + }).toPass(); + }); + + test.skip('导出表格', async ({ page, homeNavigation }) => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击顾客分配 + await page.locator('.tab_item', { hasText: '顾客分配' }).click(); + await page.locator('.assign_side').waitFor(); + // 点击更多 + await page.locator('.more', { hasText: '更多' }).waitFor(); + await page.locator('.more', { hasText: '更多' }).click(); + // 等待表显示配置 + await page.locator('.title', { hasText: '表显示配置' }).waitFor(); + const downloadPromise = page.waitForEvent('download'); + // 点击导出数据 + await page.locator('.a-modal-export', { hasText: '导出数据' }).click(); + // 等待报表导出 + await page.locator('.m-exportModal_title', { hasText: '报表导出' }).waitFor(); + // 确认 + await page.locator('.m-exportModal_button', { hasText: /^确\s认$/ }).click(); + const download = await downloadPromise; + // 获取下载的文件名 + const fileName = download.suggestedFilename(); + // 指定保存下载文件的路径 + const downloadPath = path.join(__dirname, `@/imgs/${fileName}`); + // 保存下载的文件 + await download.saveAs(downloadPath); + // 验证文件是否成功保存 + const fileExists = fs.existsSync(downloadPath); + expect(fileExists).toBeTruthy(); + }); +}); + +test.describe('顾客分析', () => { + test('查看顾客项目分析', async ({ page, homeNavigation, createCustomers, customerPage, numberInput }) => { + /**@type {Customer[]} */ + let customers = []; + await test.step('创建两个顾客', async () => { + // 创建顾客 + customers = await createCustomers(2); + }); + const ca = customers[0]; + const cb = customers[1]; + + console.log(`顾客A: ${ca.username}、${ca.phone}`); + console.log(`顾客B: ${cb.username}、${cb.phone}`); + + // 获取姓名、手机号、档案号 + const usernameA = ca.username; + const phoneA = ca.phone; + const usernameB = cb.username; + const phoneB = cb.phone; + + const ProjectA1 = ProjectName.Projects.Projects_1; + const ProjectA1Quantity = 2; + const ProjectA2 = ProjectName.Projects.Projects_239; + const ProjectA2Quantity = 1; + const ProjectA3 = ProjectName.Projects.Projects_2; + const ProjectA3Quantity = 2; + const ProjectA12Quantity = 1; + + // 顾客A + await test.step('顾客A', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(phoneA); + await customerPage.selectSearchCustomer(usernameA); + + // 购买项目1-普通,2次 + await page.getByText(ProjectA1.num).click(); + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(ProjectA1Quantity); + await numberInput.confirmValue(); + + // 点击面部 + await page.getByText('面部').click(); + + // 购买项目2-普通,1次 + await page.getByText(ProjectA2.num).click(); + await page.locator('.type_btn').first().click(); + await page.locator('.type_item', { hasText: '普通' }).click(); + + // 点击护理 + await page.getByText('护理', { exact: true }).click(); + + // 购买项目3-赠送,3次 + await page.getByText(ProjectA3.num).click(); + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(ProjectA3Quantity); + await numberInput.confirmValue(); + await page.locator('.type_btn').first().click(); + await page.locator('.type_item', { hasText: '赠送' }).click(); + + // 选择组合项目2-项目1,普通 + await page.getByText(ProjectA1.num).first().click(); + await page.locator('.add_btn', { hasText: '设置' }).last().click(); + await page.getByRole('textbox').fill(ProjectA2.num); + await page.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await page.getByLabel(ProjectA2.name).uncheck(); + await page.getByLabel(ProjectA2.name).check(); + await page.locator('.menu-item-dot', { hasText: '2' }).first().waitFor({ timeout: 2000 }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + await page.locator('.type_btn').first().click(); + await page.locator('.type_item', { hasText: '普通' }).click(); + // 消耗混合项目1次 + await page.locator('.commodity_item').last().click(); + + await page + .locator('div') + .filter({ hasText: /^结 算$/ }) + .click(); + // 使用银联支付 + await page.getByText('银联').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + }); + + const ProjectB3 = ProjectName.Projects.Projects_2; + const ProjectB3Quantity = 20; + const ProjectB4 = ProjectName.Projects.Projects_661; + const ProjectB4Quantity = 30; + const ProjectB5 = ProjectName.Projects.Projects_676; + const ProjectB5Quantity = 9; + const ProjectB1 = ProjectName.Projects.Projects_1; + const ProjectB2 = ProjectName.Projects.Projects_239; + const ProjectB12Quantity = 2; + const ProjectB34Quantity = 3; + + await test.step('顾客B', async () => { + //顾客B + await page.reload(); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(phoneB); + await customerPage.selectSearchCustomer(usernameB); + + // 购买项目3-普通,20次 + await page.getByText(ProjectB3.num).click(); + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(ProjectB3Quantity); + await numberInput.confirmValue(); + + // 点击身体 + await page.locator('.type_tab_item', { hasText: '身体' }).click(); + + // 购买项目4-普通,30次 + await page.getByText(ProjectB4.num).click(); + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(ProjectB4Quantity); + await numberInput.confirmValue(); + + // 购买项目5,9次 + await page.getByText(ProjectB5.num).click(); + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(ProjectB5Quantity); + await numberInput.confirmValue(); + + // 购买项目B1-B2混合,2次 + await page.locator('.type_tab_item', { hasText: '护理' }).click(); + await page.getByText(ProjectB1.num).first().click(); + await page.locator('.add_btn', { hasText: '设置' }).last().click(); + await page.getByRole('textbox').fill(ProjectB2.num); + await page.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await page.locator('.list_box .ant-checkbox-input').click(); + await page.locator('.menu-item-dot', { hasText: '2' }).first().waitFor({ timeout: 1000 }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + // 点击选择数量 + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(ProjectB12Quantity); + await numberInput.confirmValue(); + + // 购买项目B4-B4混合,3次 + await page.getByText(ProjectB3.num).first().click(); + await page.locator('.add_btn', { hasText: '设置' }).last().click(); + await page.getByRole('textbox').fill(ProjectB4.num); + await page.getByRole('button', { name: '搜 索' }).click(); + await expect(async () => { + await page.locator('.list_box .ant-checkbox-input').click(); + await page.locator('.menu-item-dot', { hasText: '2' }).first().waitFor({ timeout: 1000 }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + // 点击选择数量 + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(ProjectB34Quantity); + await numberInput.confirmValue(); + + await page.locator('.commodity_item').last().click(); + await page + .locator('div') + .filter({ hasText: /^结 算$/ }) + .click(); + await page.getByText('银联').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + }); + + await test.step('查看顾客项目分析', async () => { + await page.reload(); + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客分析'); + await page + .locator('div') + .filter({ hasText: /^排序$/ }) + .nth(1) + .click(); + await page.getByRole('menuitem', { name: '上次到店时间从近到远' }).click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + const Customer1A = page.locator('.m-table__body-wrapper tbody tr', { hasText: phoneA }); + const Customer1B = page.locator('.m-table__body-wrapper tbody tr', { hasText: phoneB }); + // 1 查找到顾客A 顾客B + await expect(Customer1A).toBeVisible(); + await expect(Customer1B).toBeVisible(); + + // 表格行 + const allTr = page.locator('.m-table__body-wrapper tbody tr'); + + // 获取顾客B处于第几行 + const nowRow = await page + .locator('.m-table__body-wrapper tbody tr') + .allInnerTexts() + .then(async text => { + return text.findIndex(item => item.includes(phoneB)); + }); + + // 查看各单元格内容 + // 护理内容 + await expect.soft(allTr.nth(nowRow).locator('td').nth(2)).toContainText(`${ProjectB3Quantity - 1}`); + // 身体内容 + await expect + .soft(allTr.nth(nowRow).locator('td').nth(5)) + .toContainText(`${ProjectB12Quantity + ProjectB34Quantity}`); + // 组合内容 + await expect(allTr.nth(nowRow).locator('td').nth(5)).toContainText( + `${ProjectB12Quantity + ProjectB34Quantity}`, + ); + + const $productName = page.locator('.treat_box .treat_card:nth-child(1) .name_row .auto_desc'); + const $productResidue = page.locator('.treat_box .treat_card:nth-child(1) .name_row .residue'); + const $productName2 = page.locator('.treat_box .treat_card:nth-child(2) .name_row .auto_desc'); + const $productResidue2 = page.locator('.treat_box .treat_card:nth-child(2) .name_row .residue'); + + // 点击查看弹窗内容 + // 点击护理 + await allTr.nth(nowRow).locator('td').nth(2).click(); + // 项目名称 + await expect.soft($productName).toContainText(ProjectB3.name); + // 剩余次数 + await expect($productResidue).toContainText(`${ProjectB3Quantity - 1}`); + // 关闭弹窗 + await page.locator('.close_icon').last().click(); + + // 点击身体 + await allTr.nth(nowRow).locator('td').nth(4).click(); + // 项目名称 + await expect.soft($productName).toContainText(ProjectB4.name); + // 剩余次数 + await expect.soft($productResidue).toContainText(`${ProjectB4Quantity}`); + // 项目名称 + await expect.soft($productName2).toContainText(ProjectB5.name); + // 剩余次数 + await expect.soft($productResidue2).toContainText(`${ProjectB5Quantity}`); + // 关闭弹窗 + await page.locator('.close_icon').last().click(); + + // 点击组合 + await allTr.nth(nowRow).locator('td').nth(5).click(); + // 项目名称 + await expect.soft($productName).toContainText(ProjectB1.name + ',' + ProjectB2.name); + // 剩余次数 + await expect.soft($productResidue).toContainText(`${ProjectB12Quantity}`); + // 项目名称 + await expect.soft($productName2).toContainText(ProjectB3.name + ',' + ProjectB4.name); + // 剩余次数 + await expect($productResidue2).toContainText(`${ProjectB34Quantity}`); + // 关闭弹窗 + await page.locator('.close_icon').last().click(); + + // 查看顾客A的各单元格内容 + const allTrA = page.locator('.m-table__body-wrapper tbody tr'); + const nowRowA = await allTrA.allInnerTexts().then(async text => { + return text.findIndex(item => item.includes(phoneA)); + }); + // 点击护理 + await allTrA.nth(nowRowA).locator('td').nth(2).click(); + // 项目名称 + await expect.soft($productName).toContainText(ProjectA1.name); + // 剩余次数 + await expect($productResidue).toContainText(`${ProjectA1Quantity - 1}`); + // 关闭弹窗 + await page.locator('.close_icon').last().click(); + + // 点击面部 + await allTrA.nth(nowRowA).locator('td').nth(3).click(); + // 项目名称 + await expect.soft($productName).toContainText(ProjectA2.name); + // 剩余次数 + await expect($productResidue).toContainText(`${ProjectA2Quantity}`); + // 关闭弹窗 + await page.locator('.close_icon').last().click(); + + // 点击组合 + await allTrA.nth(nowRowA).locator('td').nth(5).click(); + // 项目名称 + await expect.soft($productName).toContainText(ProjectA1.name + ',' + ProjectA2.name); + // 剩余次数 + await expect($productResidue).toContainText(`${ProjectA12Quantity}`); + // 关闭弹窗 + await page.locator('.close_icon').last().click(); + + // 点击左上角设置 + await page.locator('.setting_icon').click(); + // 选择购买数 + await page.getByText('购买数', { exact: true }).click(); + // 选择显示体验/赠送项目 + await page.getByText('显示体验/赠送项目').click(); + // 保存 + await page.getByRole('button', { name: /保\s存/ }).click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 护理内容 + const allTrAA = page.locator('.m-table__body-wrapper tbody tr'); + const nowRowAA = await allTrAA.allInnerTexts().then(async text => { + return text.findIndex(item => item.includes(phoneA)); + }); + // 点击护理 + await allTrAA.nth(nowRowAA).locator('td').nth(2).click(); + // 项目名称 + await expect.soft($productName).toContainText(ProjectA1.name); + // 剩余次数 + await expect.soft($productResidue).toContainText(`${ProjectA1Quantity - 1}`); + // 项目名称 + await expect.soft($productName2).toContainText(ProjectA3.name); + // 剩余次数 + await expect($productResidue2).toContainText(`${ProjectA3Quantity}`); + // 关闭弹窗 + await page.locator('.close_icon').last().click(); + + // 点击排序后顾客在第一页显示 + await page + .locator('div') + .filter({ hasText: /^排序$/ }) + .nth(1) + .click(); + await page.getByRole('menuitem', { name: '上次到店时间从近到远' }).click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); // 1 查找到顾客A 顾客B + await expect.soft(Customer1A).toBeVisible(); + await expect(Customer1B).toBeVisible(); + }); + }); + + test('查看项目余量分析', async ({ + page, + homeNavigation, + customerPage, + createCustomer, + customerAnalysisPage, + numberInput, + }) => { + const customer = createCustomer; + let billNo = ''; + + // 分类-项目选择器 + const $$switch = page.locator('.coustom_switch'); + const $categoryAndProject = $$switch + .filter({ hasText: '分类' }) + .filter({ hasText: '项目' }) + .getByRole('switch'); + // 客数-余次选择器 + const $passengersAndRemainder = $$switch + .filter({ hasText: '客数' }) + .filter({ hasText: '余次' }) + .getByRole('switch'); + + await test.step('进入顾客分析-》项目余量分析', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客分析'); + await customerAnalysisPage.gotoSubPage('项目余量分析'); + }); + + let project = { name: '', className: '' }; + + await test.step('获取项目中客数最高的项目', async () => { + await page.getByRole('cell', { name: '普通' }).locator('svg').nth(1).click(); + await page.getByRole('cell', { name: '类别' }).waitFor(); + const $firstProjectTr = page.locator('.main-table-body_tr').first(); + await $firstProjectTr.waitFor(); + const $firstProjectTd = $firstProjectTr.locator('td'); + // 获取项目名称和项目类型名称 + project.name = (await $firstProjectTd.first().innerText()).trim(); + project.className = (await $firstProjectTd.nth(1).innerText()).trim(); + }); + + expect(project.name).not.toBe(''); + expect(project.className).not.toBe(''); + + await test.step('进入顾客详情', async () => { + await customerPage.gotoSubPage('顾客概要'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + }); + + await test.step('开单购买普通、赠送、体验各三次,并且各消耗一次', async () => { + // 项目列表 + const $$project = page.locator('.project_list'); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + await page.getByRole('main').getByText(project.className, { exact: true }).click(); + + // 普通三次 + await $$project.getByText(project.name, { exact: true }).click(); + await page.locator('#buyList').getByText('1', { exact: true }).click(); + await numberInput.setValue(3); + await numberInput.confirmValue(); + + // 体验三次 + await $$project.getByText(project.name, { exact: true }).click(); + await page.locator('#buyList').getByText('1', { exact: true }).click(); + await numberInput.setValue(3); + await numberInput.confirmValue(); + await page.getByRole('button', { name: '普' }).click(); + await page.getByText('体验', { exact: true }).click(); + + // 赠送三次 + await $$project.getByText(project.name, { exact: true }).click(); + await page.locator('#buyList').getByText('1', { exact: true }).click(); + await numberInput.setValue(3); + await numberInput.confirmValue(); + await page.getByRole('button', { name: '普' }).click(); + await page.getByText('赠送', { exact: true }).click(); + + // 普通、体验、赠送各消耗三次 + await page.locator('#shoppingCart').getByText('雪肌晶纯护理').first().click(); + await page.locator('#shoppingCart').getByText('雪肌晶纯护理').nth(1).click(); + await page.locator('#shoppingCart').getByText('雪肌晶纯护理').nth(2).click(); + + // 结算 + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + await page.locator('.paymentInfoItem', { hasText: '现金' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /^结\s算$/ }).click(), + ]); + + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + expect(billNo).not.toBeNull(); + console.log('Bill No:' + billNo); + + await expect(page.getByRole('button', { name: /开\s单/ })).toBeInViewport(); + }); + + await test.step('进入顾客分析-》项目余量分析', async () => { + await homeNavigation.gotoModule('顾客'); + await customerPage.gotoSubPage('顾客分析'); + await customerAnalysisPage.gotoSubPage('项目余量分析'); + }); + + await test.step('查看类别', async () => { + await page.getByText('选择项目类别').click(); + await page.getByRole('option', { name: project.className }).click(); + + await Promise.all([ + waitSpecifyApiLoad(page, ['/property_analysis']), + page.getByRole('cell', { name: '普通' }).locator('svg').nth(1).click(), + ]); + // 判断项目存在 + await expect(page.getByRole('cell', { name: project.name, exact: true })).toBeVisible(); + }); + + // 项目所在行 + const $projectTr = page + .locator('.main-table-body_tr') + .filter({ has: page.getByText(project.name) }) + .filter({ has: page.getByText(project.className) }); + + // 弹窗内容 + const $popup = page.locator('.popup_content'); + // 弹窗内顾客的信息 + const $customerTr = $popup + .locator('.main-table-body_tr') + .filter({ has: page.getByText(customer.username) }) + .filter({ has: page.getByText(customer.phone) }); + + //普通 + const $normalProjectTd = $projectTr.getByRole('cell').nth(2); + //体验 + const $experienceProjectTd = $projectTr.getByRole('cell').nth(3); + //赠送 + const $giveAwayProjectTd = $projectTr.getByRole('cell').nth(4); + + await test.step('查看数据', async () => { + // 普通 + await $normalProjectTd.click(); + await expect($customerTr).toBeVisible(); + await page.locator('.title > .close_icon > svg').click(); + + // 体验 + await $experienceProjectTd.click(); + await expect($customerTr).toBeVisible(); + await page.locator('.title > .close_icon > svg').click(); + + // 赠送 + await $giveAwayProjectTd.click(); + await expect($customerTr).toBeVisible(); + await page.locator('.title > .close_icon > svg').click(); + }); + + // 顾客项目分析数据 + const analysisData = { + normal: -1, + experience: -1, + giveAway: -1, + }; + + // 项目剩余次数 + const $currentItemRemaining = page + .locator('.proerty_summery .item') + .filter({ has: page.getByText('项目余次') }) + .locator('.item_num'); + + await test.step('显示已用完的项目、余次,查看数据', async () => { + // 配置为显示已用完的项目和项目余次 + await $passengersAndRemainder.click(); + await page.locator('.setting_icon').click(); + await page.getByLabel('显示已用完的项目').check(); + await Promise.all([ + waitSpecifyApiLoad(page, ['/property_analysis']), + page.getByRole('button', { name: /保\s存/ }).click(), + ]); + + // 获取项目余次数据 + analysisData.normal = Number((await $normalProjectTd.innerText()).trim()); + analysisData.experience = Number((await $experienceProjectTd.innerText()).trim()); + analysisData.giveAway = Number((await $giveAwayProjectTd.innerText()).trim()); + + // 普通 + await $normalProjectTd.click(); + await expect($customerTr).toBeVisible(); + const currentNormalData = Number((await $currentItemRemaining.innerText()).trim()); + expect(currentNormalData).toBeGreaterThanOrEqual(analysisData.normal); + await page.locator('.title > .close_icon > svg').click(); + + // 体验 + await $experienceProjectTd.click(); + await expect($customerTr).toBeVisible(); + const currentExperienceData = Number((await $currentItemRemaining.innerText()).trim()); + expect(currentExperienceData).toBeGreaterThanOrEqual(analysisData.experience); + await page.locator('.title > .close_icon > svg').click(); + + // 赠送 + await $giveAwayProjectTd.click(); + await expect($customerTr).toBeVisible(); + const currentGiveAwayData = Number((await $currentItemRemaining.innerText()).trim()); + expect(currentGiveAwayData).toBeGreaterThanOrEqual(analysisData.giveAway); + await page.locator('.title > .close_icon > svg').click(); + }); + + // 项目剩余次数行 + const $categoryTr = page.locator('.main-table-body_tr').filter({ has: page.getByText(project.className) }); + //普通 + const $normalProjectTdForCategory = $categoryTr.getByRole('cell').nth(1); + //体验 + const $experienceProjectTdForCategory = $categoryTr.getByRole('cell').nth(2); + //赠送 + const $giveAwayProjectTdForCategory = $categoryTr.getByRole('cell').nth(3); + + await test.step('分类、客数,查看数据', async () => { + // 切换为分类和客数 + await $categoryAndProject.click(); + await $passengersAndRemainder.click(); + + // 普通 + await $normalProjectTdForCategory.click(); + await expect($customerTr).toBeVisible(); + await page.locator('.title > .close_icon > svg').click(); + + // 体验 + await $experienceProjectTdForCategory.click(); + await expect($customerTr).toBeVisible(); + await page.locator('.title > .close_icon > svg').click(); + + // 赠送 + await $giveAwayProjectTdForCategory.click(); + await expect($customerTr).toBeVisible(); + await page.locator('.title > .close_icon > svg').click(); + }); + }); + + test('查看套餐消耗升单分析', async ({ page, homeNavigation, createCustomer, customerPage, numberInput }) => { + const c = createCustomer; + + // 点击顾客 拿取初始数据 + await homeNavigation.gotoModule('顾客'); + await page.locator('.tab_item', { hasText: '顾客分析' }).click(); + await page.getByText('顾客项目分析').click(); + await page.getByRole('menuitem', { name: '套餐消耗升单分析' }).click(); + // 选择套餐 + await page.locator('.selectpage').click(); + await page.getByLabel('A套餐(GK035)').check(); + await page.getByRole('button', { name: '确定选择' }).click(); + await page.locator('span').filter({ hasText: '选择员工' }).click(); + await page.getByLabel('陈刚').check(); + await page.getByRole('button', { name: '确定选择' }).click(); + await page.getByRole('button', { name: '查询' }).click(); + // 点击查看购买消耗升单 + await page.locator('.table_top_center').click(); + // 初始购买 + const Initialbuy = await page.locator('.propomTop .propomSize').nth(0).innerText(); //购买 + // 初始消耗 + const Initialexpend = await page.locator('.propomTop .propomSize').nth(1).innerText(); //消耗 + // 初始升单 + const Initialupbills = await page.locator('.propomTop .propomSize').nth(2).innerText(); //升单 + // 关闭窗口 + await page + .locator('div') + .filter({ hasText: /^A套餐(GK035)合计$/ }) + .locator('i') + .click(); + + await page.reload(); + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(c.phone); + await customerPage.selectSearchCustomer(c.username); + + // 选择套餐 + await page.locator('.float_tab').getByText('套餐').click(); + // 选择A套餐 + await page.locator('.project_one > .item').filter({ hasText: ProjectName.SetMeal.SetMeal_6.name }).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + // 选择员工1 + const employee1 = Employees.FirstShop.Employee_6.name; + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目/卖品' }).click(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + // 点击现金 + await page.getByText('现金').click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await expect.soft(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(c.phone); + await customerPage.selectSearchCustomer(c.username); + // 消耗该项目5次 + await page.locator('.treat_card_content').first().click(); + // 点击选择数量 + await page.locator('.buy_number').click(); + // 点击删除所有数量 + await page.locator('div').filter({ hasText: /^123$/ }).getByRole('button').nth(3).click(); + // 选择数量5 + await numberInput.setValue(5); + await numberInput.confirmValue(); + // 点击添加员工 + await page.locator('.staff_btn').click(); + // 选择员工1 + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 保存并复制到其他项目 + await page.locator('button.save_and_copy').filter({ hasText: '保存并复制到其他项目' }).click(); + // 购买项目C + await page.getByText(ProjectName.Projects.Projects_21.num).click(); + // 点击选择数量 + await page.locator('.edit_txt div:nth-child(2)').first().click(); + // 点击删除所有数量 + await page.locator('div').filter({ hasText: /^123$/ }).getByRole('button').nth(3).click(); + // 选择数量2 + await numberInput.setValue(2); + await numberInput.confirmValue(); + + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + // 点击现金 + await page.getByText('现金').click(); + // 取消推送消息提醒 + await page.getByLabel('推送消费提醒').uncheck(); + // 取消结算签字 + await page.getByLabel('结算签字').uncheck(); + // 点击结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect.soft(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + + await homeNavigation.gotoModule('顾客'); + await page.locator('.tab_item', { hasText: '顾客分析' }).click(); + await page.getByText('顾客项目分析').click(); + await page.getByRole('menuitem', { name: '套餐消耗升单分析' }).click(); + // 选择套餐 + await page.locator('.selectpage').click(); + await page.getByLabel('A套餐(GK035)').check(); + await page.getByRole('button', { name: '确定选择' }).click(); + await page.locator('.search_middle .select_multiple').click(); + await page.getByLabel('陈刚').check(); + await page.getByRole('button', { name: '确定选择' }).click(); + await page.getByRole('button', { name: '查询' }).click(); + // 点击查看购买消耗升单 + await page.locator('.table_top_center').click(); + await expect.soft(page.locator('.propomTop .propomSize').nth(0)).toContainText(`${Number(Initialbuy) + 1}`); + await expect.soft(page.locator('.propomTop .propomSize').nth(1)).toContainText(`${Number(Initialexpend) + 1}`); + await expect(page.locator('.propomTop .propomSize').nth(2)).toContainText(`${Number(Initialupbills) + 1}`); + // 关闭窗口 + await page + .locator('div') + .filter({ hasText: /^A套餐(GK035)合计$/ }) + .locator('i') + .click(); + + // 顾客行数 + const allTr = page.locator('.m-table__body-wrapper tbody tr'); + await allTr.first().waitFor(); + let nowRow = await allTr.allInnerTexts().then(async text => { + return text.findIndex(item => item.includes(c.phone)); + }); + + const ProjectA = ProjectName.Projects.Projects_19.name; + const ProjectB = ProjectName.Projects.Projects_20.name; + const ProjectC = ProjectName.Projects.Projects_21.name; + // 员工(陈刚) + await expect( + page.locator('.m-table__body-wrapper tbody tr').nth(nowRow).locator('td:nth-child(4)'), + ).toContainText(employee1); + // 套餐消耗(水娃娃x5) + await expect( + page.locator('.m-table__body-wrapper tbody tr').nth(nowRow).locator('td:nth-child(5) .projectName'), + ).toContainText(ProjectA + 'x5'); + // 升单(德国原装冰吻之恋x2) + await expect( + page.locator('.m-table__body-wrapper tbody tr').nth(nowRow).locator('td:nth-child(6) .projectName'), + ).toContainText(ProjectC + 'x2'); + // 套餐结余(水娃娃x5 德国原装水元素x5) + await expect( + page.locator('.m-table__body-wrapper tbody tr').nth(nowRow).locator('td:nth-child(7)'), + ).toContainText(ProjectA + 'x5'); + await expect( + page.locator('.m-table__body-wrapper tbody tr').nth(nowRow).locator('td:nth-child(7)'), + ).toContainText(ProjectB + 'x5'); + }); +}); + +test.describe('批量操作', () => { + let usePhones = Array(8).fill(null); // 创建一个长度为8的数组,初始值为null(表示未设置) + + test.beforeEach(async ({ page }) => { + // 监听响应事件 + await page.addLocatorHandler( + page.locator('.popup_content .ant-btn-primary', { hasText: '我知道了' }), + async () => { + await page.locator('.popup_content .ant-btn-primary').click(); + await expect(page.locator('.popup_content .ant-btn-primary')).not.toBeVisible(); + }, + ); + }); + + test('顾客分配', async ({ page, homeNavigation }) => { + await page.reload(); + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 打开顾客列表 + // await page.getByRole("menuitem", { name: "打开顾客列表" }).click(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第二页 + await page.locator('.ant-pagination-item-2').click(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 顾客分配 + await page.getByRole('menuitem', { name: '顾客分配' }).click(); + await expect(async () => { + try { + await page.locator('.label_text', { hasText: '移除已分配咨询师' }).waitFor({ timeout: 2000 }); + await page.locator('.label_text', { hasText: '移除已分配咨询师' }).click(); + } catch { + // 关闭窗口 + await page.locator('.ant-modal-close-x').click(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 顾客分配 + await page.getByRole('menuitem', { name: '顾客分配' }).click(); + await page.locator('.label_text', { hasText: '移除已分配咨询师' }).waitFor({ timeout: 2000 }); + await page.locator('.label_text', { hasText: '移除已分配咨询师' }).click(); + } + }).toPass(); + // // 点击已经已分配 + // await page.locator(".label_text", { hasText: "移除已分配咨询师" }).click(); + // 确认分配 + await page.getByRole('button', { name: '确认分配' }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + // 确保清完已分配重新进入进行分配 + await page.reload(); + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 打开顾客列表 + // await page.getByRole("menuitem", { name: "打开顾客列表" }).click(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第二页 + await page.locator('.ant-pagination-item-2').click(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 顾客分配 + await page.getByRole('menuitem', { name: '顾客分配' }).click(); + // 选择员工A + let nowRowA = 0; + const employee1 = Employees.FirstShop.Employee_3.name; + const allTrA = page.locator('.ant-tree-child-tree-open').first().locator('.ant-tree-treenode-switcher-open'); + const countA = await allTrA.count(); + // 获取第几个 + for (let i = 0; i < countA; i++) { + const trA = allTrA.nth(i); + const employee = await trA.locator('.ant-tree-title').innerText(); + console.log(employee); + if (employee.includes(employee1)) { + nowRowA = i; + break; + } + } + await page + .locator('.ant-tree-child-tree-open') + .first() + .locator('.ant-tree-treenode-switcher-open') + .nth(nowRowA) + .click(); + + // 选择员工B + let nowRowB = 0; + const employee2 = Employees.FirstShop.Employee_4.name; + const allTrB = page.locator('.ant-tree-child-tree-open').first().locator('.ant-tree-treenode-switcher-open'); + const countB = await allTrB.count(); + // 获取第几个 + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const employeeB = await trB.locator('.ant-tree-title').innerText(); + console.log(employeeB); + if (employeeB.includes(employee2)) { + nowRowB = i; + break; + } + } + await page + .locator('.ant-tree-child-tree-open') + .first() + .locator('.ant-tree-treenode-switcher-open') + .nth(nowRowB) + .click(); + + // 确认分配 + await page.getByRole('button', { name: '确认分配' }).click(); + // 操作成功 + await expect(page.locator('.ant-message-notice', { hasText: '操作成功' })).toBeVisible(); + + await page.reload(); + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 打开顾客列表 + // await page.getByRole("menuitem", { name: "打开顾客列表" }).click(); + // 点击第一个会员 + await page.locator('.m-table__fixed-left .user_info').first().click(); + // 等待基本资料加载完成 + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断这两员工是否分配到了 + const employeeResult = await page.locator('.description .employees_txt').innerText(); + expect(employeeResult).toBe(employee1 + '、' + employee2); + // 关闭详情回到顾客列表 + await page.locator('.close_icons').click(); + // 点击第二页 + await page.locator('.ant-pagination-item-2').click(); + // 点击第一个会员 + await page.locator('.m-table__fixed-left .user_info').first().click(); + // 等待基本资料加载完成 + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断这两员工是否分配到了 + expect(employeeResult).toBe(employee1 + '、' + employee2); + + await page.reload(); + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第二页 + await page.locator('.ant-pagination-item-2').click(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 顾客分配 + await page.getByRole('menuitem', { name: '顾客分配' }).click(); + // 点击已经已分配 + await page.locator('.label_text', { hasText: '移除已分配咨询师' }).click(); + // 确认分配 + await page.getByRole('button', { name: '确认分配' }).click(); + // 操作成功 + await expect(page.locator('.ant-message-notice', { hasText: '操作成功' })).toBeVisible(); + }); + + test('新增待办', async ({ page, homeNavigation }) => { + // 随机数字备注 + const remark = faker.helpers.fromRegExp(/1[3-9][0-9]{7}/); + + await test.step('勾选会员并记录该会员手机', async () => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 获取准备选中的会员手机1 + await page.locator('.m-table-pagination .dec').waitFor(); + const member = page.locator('.m-table__fixed-left tbody tr'); + usePhones[0] = await member.locator('td .user_info_body').first().innerText(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第下一页 + await page.locator('.ant-pagination-next').click(); + // 获取准备选中的会员手机2 + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + usePhones[1] = await member.locator('td .user_info_body').first().innerText(); + console.log('手机1=' + usePhones[0] + '手机2=' + usePhones[1]); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + }); + + await test.step('批量操作新增待办', async () => { + // 判断批量操作出现 + const Batch = page.locator('.item-btns', { hasText: '批量操作' }); + await expect(Batch).toBeVisible(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 点击新增代办事项 + await page.getByRole('menuitem', { name: '新增待办事项' }).click(); + await page.locator('.a_title').waitFor(); + const title = (await page.locator('.a_title').innerText()).trim(); + expect.soft(title).toBe('批量新增事项'); + await expect(async () => { + await page.getByPlaceholder('请选择日期').click(); + await page.locator('.date_picker_today', { hasText: '今天' }).click(); + await page.locator('.normal', { hasText: '今天' }).waitFor({ timeout: 2000 }); + }).toPass(); + + // 选择类型 + await page.locator('.ant-radio-button-wrapper', { hasText: '电话回访' }).click(); + // 填入随机备注 + await page.getByPlaceholder('请输入1-100个字符的备注内容').fill(remark); + await page.locator('.shop_btn', { hasText: '确认添加' }).click(); + await page.locator('.ant-message', { hasText: '操作成功' }).waitFor(); + await expect.soft(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + + await test.step('校验数据', async () => { + // 点击第一个会员 + await page.locator('.m-table__fixed-left .m-table__body tbody tr').first().locator('td').nth(1).click(); + await page.locator('.consume_data').waitFor(); + // 点击动态 + await page.locator('.ant-tabs-nav-animated .ant-tabs-tab', { hasText: '动态' }).click(); + await page.locator('.ant-fullcalendar-tbody').first().waitFor(); + // 切换列表模式 + if (await page.locator('.selected_btn', { hasText: '日历模式' }).isVisible()) { + await page.locator('.date_change').first().click(); + } else { + console.log('无需切换'); + } + await page.locator('.ant-tabs-tabpane-active .m-table__body-wrapper').waitFor(); + const success = page.locator('.ant-tabs-tabpane-active .m-table__body-wrapper .m-table__body tbody tr'); + const success_color = await success + .filter({ has: page.locator('td', { hasText: remark }) }) + .locator('td') + .nth(2) + .innerText(); + expect.soft(success_color).toBe('已完成'); + await page.locator('.close_icons').click(); + }); + }); + + test('设置标签', async ({ page, homeNavigation }) => { + await test.step('勾选会员并记录该会员手机', async () => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 获取准备选中的会员手机1 + await page.locator('.m-table-pagination .dec').waitFor(); + const member = page.locator('.m-table__fixed-left tbody tr'); + usePhones[2] = await member.locator('td .user_info_body').first().innerText(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第下一页 + await page.locator('.ant-pagination-next').click(); + // 获取准备选中的会员手机2 + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + usePhones[3] = await member.locator('td .user_info_body').first().innerText(); + console.log('手机1=' + usePhones[2] + '手机2=' + usePhones[3]); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + }); + + await test.step('批量操作设置标签', async () => { + // 判断批量操作出现 + const Batch = page.locator('.item-btns', { hasText: '批量操作' }); + await expect(Batch).toBeVisible(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 点击设置标签 + await page.getByRole('menuitem', { name: '设置标签' }).click(); + // 等待选择标签出现 + await page.locator('.ant-modal-title', { hasText: '选择标签' }).waitFor(); + // 点击两次标题全选 全取消 + await page.locator('.ant-tree-title', { hasText: '顾客六期管理' }).click(); + const check = page.locator('.ant-tree-checkbox-checked').first(); + await expect(check).toBeVisible(); + await page.locator('.ant-tree-title', { hasText: '顾客六期管理' }).click(); + await expect(check).not.toBeVisible(); + // 选择铺垫期、销售期 + await page.locator('.ant-tree-title', { hasText: '铺垫期' }).click(); + await page.locator('.ant-tree-node-selected', { hasText: '铺垫期' }).waitFor(); + await page.locator('.ant-tree-title', { hasText: '销售期' }).click(); + await page.locator('.ant-tree-node-selected', { hasText: '销售期' }).waitFor(); + // 确认 + await page.locator('.ok_btn', { hasText: /^确\s认$/ }).click(); + }); + + await test.step('校验数据', async () => { + // 根据获取的手机号码搜索该会员 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(usePhones[2]); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + // 等待会员数据弹出 + await page.locator('.alertBox .close').waitFor(); + // 根据指定手机点击会员 + await page.locator('.member_list_li').filter({ hasText: usePhones[2] }).click(); + // 点击进入详情 + await page.locator('.m-table__fixed-left .user_info_body', { hasText: usePhones[2] }).click(); + // 等待基础资料特定界面出现 + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断标签显示是否正确 + const pdq = await page.locator('.sign_txt', { hasText: '铺垫期' }).innerText(); + expect(pdq).toBe('铺垫期'); + const xxq = await page.locator('.sign_txt', { hasText: '销售期' }).innerText(); + expect(xxq).toBe('销售期'); + // 顺带去掉该标签 + await page.locator('.sign .edit_icon').click(); + await page.locator('.ant-tree-title', { hasText: '顾客六期管理' }).click(); + const checks = page.locator('.ant-tree-checkbox-checked').first(); + await expect(checks).toBeVisible(); + await page.locator('.ant-tree-title', { hasText: '顾客六期管理' }).click(); + await expect(checks).not.toBeVisible(); + await page.locator('.ok_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.close_icons').click(); + + // 根据获取手机搜索会员 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(usePhones[3]); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + // 等待会员数据弹出 + await page.locator('.alertBox .close').waitFor(); + // 根据指定手机点击会员 + await page.locator('.member_list_li').filter({ hasText: usePhones[3] }).click(); + // 点击进入详情 + await page.locator('.m-table__fixed-left .user_info_body', { hasText: usePhones[3] }).click(); + // 等待基础资料特定界面出现 + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断标签显示是否正确 + expect.soft(pdq).toBe('铺垫期'); + expect(xxq).toBe('销售期'); + // 顺带去掉该标签 + await page.locator('.sign .edit_icon').click(); + await page.locator('.ant-tree-title', { hasText: '顾客六期管理' }).click(); + await expect(checks).toBeVisible(); + await page.locator('.ant-tree-title', { hasText: '顾客六期管理' }).click(); + await expect(checks).not.toBeVisible(); + await page.locator('.ok_btn', { hasText: /^确\s认$/ }).click(); + }); + }); + + test('设置无效客', async ({ page, homeNavigation, customerPage }) => { + let usernameA; + let phoneA; + const ca = new Customer(1, 1); + await test.step('创建会员', async () => { + // 创建顾客 + await customerPage.createCustomer(ca); + usernameA = ca.username; + phoneA = ca.phone; + }); + + await test.step('搜索创建的会员设置无效客', async () => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(phoneA); + await expect(async () => { + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.alertBox .close').waitFor(); + await page.locator('.member_list_li').filter({ hasText: usernameA }).click(); + const useinfo = page.locator('.member_list_li').filter({ hasText: usernameA }); + await expect(useinfo).not.toBeVisible(); + const member = page.locator('.m-table__fixed-left tbody tr'); + await member.locator('td .user_info_body', { hasText: phoneA }).first().waitFor(); + }).toPass(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + + // 判断批量操作出现 + const Batch = page.locator('.item-btns', { hasText: '批量操作' }); + await expect(Batch).toBeVisible(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 点击设为无效客 + await page.getByRole('menuitem', { name: '设为无效客' }).click(); + // 确认 + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + + await test.step('高级搜索无效客', async () => { + // 点击高级搜索 + await page.locator('.ant-btn-default', { hasText: /^高\s级$/ }).click(); + await page.locator('.shopSelect_title', { hasText: '顾客高级查询' }).waitFor(); + // 点击两次全选确保所有门店默认未选 + await page + .locator('.item_sub') + .filter({ has: page.locator('.item_name', { hasText: '所属门店' }) }) + .locator('.item_val') + .click(); + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + // 判断某个门店被选中 + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page + .locator('.comPicker_btn', { hasText: /^全\s选$/ }) + .first() + .click(); + // 判断门店未被选中 + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).not.toBeVisible(); + await page.locator('.label_checkbox', { hasText: 'AT测试一店' }).click(); + // 确认选中一店 + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible(); + await page.locator('.ant-btn-primary', { hasText: '确定选择' }).nth(1).click(); + // 搜索关键字手机 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(phoneA); + await page + .locator('.ant-checkbox-wrapper') + .filter({ has: page.locator('.needsclick', { hasText: '无效客' }) }) + .locator('.ant-checkbox') + .click(); + const Invalid = page.locator('.ant-checkbox-wrapper-checked', { hasText: '无效客' }); + // 判断无效客选中 + await expect(Invalid).toBeVisible(); + // 点击搜索 + await page.locator('.ant-btn-block').click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + const $use = page.locator('.m-table__fixed-left tbody tr').first(); + await expect(async () => { + if (await $use.isVisible()) { + await expect(page.locator('.m-table__fixed-left tbody tr', { hasText: phoneA })).toBeVisible(); + } else { + // 点击高级搜索 + await page.locator('.ant-btn-default', { hasText: /^高\s级$/ }).click(); + await page.locator('.shopSelect_title', { hasText: '顾客高级查询' }).waitFor(); + // 点击搜索 + await page.locator('.ant-btn-block').click(); + } + }).toPass(); + }); + }); + + test('设置会员分类', async ({ page, homeNavigation }) => { + await test.step('勾选会员并记录该会员手机', async () => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 获取准备选中的会员手机1 + await page.locator('.m-table-pagination .dec').waitFor(); + const member = page.locator('.m-table__fixed-left tbody tr'); + usePhones[4] = await member.locator('td .user_info_body').first().innerText(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第下一页 + await page.locator('.ant-pagination-next').click(); + // 获取准备选中的会员手机2 + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + usePhones[5] = await member.locator('td .user_info_body').first().innerText(); + console.log('手机1=' + usePhones[4] + '手机2=' + usePhones[5]); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + }); + + await test.step('批量操作设置会员分类', async () => { + // 判断批量操作出现 + const Batch = page.locator('.item-btns', { hasText: '批量操作' }); + await expect(Batch).toBeVisible(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 点击设置会员分类 + await page.getByRole('menuitem', { name: '设置会员分类' }).click(); + // 等待设置会员分类出现 + await page.locator('.ant-model-title', { hasText: '设置会员分类' }).waitFor(); + await expect(async () => { + // 选择铺(铺垫期) + await page.locator('.class_tag .tag', { hasText: '铺' }).click(); + const check = page.locator('.select_style'); + await expect(check).toBeVisible({ timeout: 2000 }); + // 确认 + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message', { hasText: '操作成功' }).waitFor({ timeout: 5000 }); + }).toPass(); + }); + + await test.step('校验数据', async () => { + // 根据获取的手机号码搜索该会员 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(usePhones[4]); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + // 等待会员数据弹出 + await page.locator('.alertBox .close').waitFor(); + // 根据指定手机点击会员 + await page.locator('.member_list_li').filter({ hasText: usePhones[4] }).click(); + // 点击进入详情 + await page.locator('.m-table__fixed-left .user_info_body', { hasText: usePhones[4] }).click(); + // 等待基础资料特定界面出现 + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断分类显示是否正确 + const sort = page.locator('.tag_list .class_icon .icon_style', { hasText: '铺' }); + await expect(sort).toBeVisible(); + // 去掉该分类判断一下是否消失 + // 点击三点 + await page.locator('.more_icon svg').click(); + // 选择会员分类设置 + await page.locator('.ant-dropdown-menu-item', { hasText: '设置会员分类' }).click(); + // 等待设置会员分类出现 + await page.locator('.ant-model-title', { hasText: '设置会员分类' }).waitFor(); + // 点击被选中的分类 + await page.locator('.select_style').click(); + // 判断选中分类已取消 + await expect(page.locator('.select_style')).not.toBeVisible(); + // 确认 + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + // 关闭详情 + await page.locator('.close_icons').click(); + + // 根据获取的手机号码搜索该会员 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(usePhones[5]); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + // 等待会员数据弹出 + await page.locator('.alertBox .close').waitFor(); + // 根据指定手机点击会员 + await page.locator('.member_list_li').filter({ hasText: usePhones[5] }).click(); + // 点击进入详情 + await page.locator('.m-table__fixed-left .user_info_body', { hasText: usePhones[5] }).click(); + // 等待基础资料特定界面出现 + await page.locator('.basic_box .base_info').first().waitFor(); + // 判断分类显示是否正确 + await expect.soft(sort).toBeVisible(); + // 去掉该分类判断一下是否消失 + // 点击三点 + await page.locator('.more_icon svg').click(); + // 选择会员分类设置 + await page.locator('.ant-dropdown-menu-item', { hasText: '设置会员分类' }).click(); + // 等待设置会员分类出现 + await page.locator('.ant-model-title', { hasText: '设置会员分类' }).waitFor(); + // 点击被选中的分类 + await page.locator('.select_style').click(); + // 判断选中分类已取消 + await expect(page.locator('.select_style')).not.toBeVisible(); + // 确认 + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + // 关闭详情 + await page.locator('.close_icons').click(); + }); + }); + + test('赠送积分', async ({ page, homeNavigation, numberInput }) => { + //输入积分 + const points = 8; + + await test.step('勾选会员并记录该会员手机', async () => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 获取准备选中的会员手机1 + await page.locator('.m-table-pagination .dec').waitFor(); + const member = page.locator('.m-table__fixed-left tbody tr'); + usePhones[6] = await member.locator('td .user_info_body').first().innerText(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第下一页 + await page.locator('.ant-pagination-next').click(); + // 获取准备选中的会员手机2 + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + usePhones[7] = await member.locator('td .user_info_body').first().innerText(); + console.log('手机1=' + usePhones[6] + '手机2=' + usePhones[7]); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + }); + + await test.step('批量操作赠送积分', async () => { + // 判断批量操作出现 + const Batch = page.locator('.item-btns', { hasText: '批量操作' }); + await expect(Batch).toBeVisible(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 点击赠送积分 + await page.getByRole('menuitem', { name: '赠送积分' }).click(); + // 等待赠送积分出现 + await page.locator('.popup_content .title', { hasText: '赠送积分' }).waitFor(); + + // 输入积分 + await numberInput.setValue(points); + await numberInput.confirmValue(); + + await page.locator('.ant-message-custom-content', { hasText: '赠送成功,预计10分钟内到账' }).waitFor(); + await expect( + page.locator('.ant-message-custom-content', { + hasText: '赠送成功,预计10分钟内到账', + }), + ).toBeVisible(); + }); + + await test.step('校验核对', async () => { + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(usePhones[6]); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + // 等待会员数据弹出 + await page.locator('.alertBox .close').waitFor(); + // 根据指定手机点击会员 + await page.locator('.member_list_li').filter({ hasText: usePhones[6] }).click(); + // 点击进入详情 + await page.locator('.m-table__fixed-left .user_info_body', { hasText: usePhones[6] }).click(); + // 等待基础资料特定界面出现 + await page.locator('.basic_box .base_info').first().waitFor(); + // 详情页点击流水 + await page.locator('.ant-tabs-tab', { hasText: '流水' }).click(); + // 等待页面加载 + await page.locator('.tab_btn .ant-radio-button-wrapper').first().waitFor(); + // 点击积分记录 + await page.locator('.tab_btn .ant-radio-button-wrapper', { hasText: '积分记录' }).click(); + // 获取积分变动 + await expect(page.locator('.positive').first()).toContainText(`${points}`); + }); + }); + + test('群发优惠券', async ({ page, homeNavigation }) => { + // 用于存储会员名称 + const useNames = []; + await test.step('勾选会员并记录该会员手机', async () => { + // 进入顾客界面 + await homeNavigation.gotoModule('顾客'); + // 点击潜客 + await page.locator('.sub_icon').nth(3).click(); + // 打开顾客列表 + // 获取准备选中的会员手机1 + await page.locator('.m-table-pagination .dec').waitFor(); + const member = page.locator('.m-table__fixed-left tbody tr'); + useNames[0] = await member.locator('td .user_name').first().innerText(); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + // 点击第下一页 + await page.locator('.ant-pagination-next').click(); + // 获取准备选中的会员手机2 + await expect.soft(page.locator('.m-table__icon__warp')).toBeHidden(); + useNames[1] = await member.locator('td .user_name').first().innerText(); + console.log('姓名1=' + useNames[0] + '姓名2=' + useNames[1]); + // 选中第一个顾客 + await page.locator('.m-table__fixed-left .is-center').nth(1).click(); + }); + + await test.step('批量操作赠送积分', async () => { + // 判断批量操作出现 + const Batch = page.locator('.item-btns', { hasText: '批量操作' }); + await expect(Batch).toBeVisible(); + // 批量操作 + await page.locator('.item-btns', { hasText: /^批量操作$/ }).click(); + // 点击群发优惠券 + await page.getByRole('menuitem', { name: '群发优惠券' }).click(); + // 等待群发优惠券出现 + await page.locator('.m_sliding_menu .header', { hasText: '群发优惠券' }).waitFor(); + // 点击添加优惠券 + await page.locator('.add_txt', { hasText: '添加优惠券' }).click(); + await page.locator('.popup_content .title', { hasText: '选择优惠券' }).waitFor(); + // 选择优惠券 + await page.locator('.check_item').click(); + // 判断是否选中 + const coupon = page.locator('.ant-checkbox-checked').first(); + await expect(coupon).toBeVisible(); + // 保存 + await page.locator('.confirm_btn', { hasText: /^保\s存$/ }).click(); + const coupons = page.locator('.coupon_list').first(); + await expect(coupons).toBeVisible(); + // 确认发送 + await page.locator('.sure_btn span', { hasText: '确认发送' }).click(); + await page.locator('.handle_task_content').waitFor(); + // 点击顾客列表 + await page.locator('.m-table__fixed-right tbody .gustom_list_btn').first().click(); + // 等待发送顾客列表 + await page.locator('.custom_list_content .header').first().waitFor(); + await expect.soft(page.locator('.custom_name').first()).toContainText(useNames[0]); + await expect(page.locator('.custom_name').last()).toContainText(useNames[1]); + }); + }); +}); diff --git a/tests/touch/boss_goal.spec.ts b/tests/touch/boss_goal.spec.ts new file mode 100644 index 0000000..d304554 --- /dev/null +++ b/tests/touch/boss_goal.spec.ts @@ -0,0 +1,1501 @@ +// @ts-check +import { expect, test } from '@/fixtures/boss_common.js'; +import { staffData } from '@/fixtures/staff.js'; +import { Employees } from '@/fixtures/userconfig.js'; +import { Customer } from '@/utils/customer'; +import { KeepOnlyNumbers } from '@/utils/utils.js'; + +test.describe('目标设置', () => { + test('总部设置', async ({ page, homeNavigation, numberInput }) => { + const goal = 100; //目标金额 + const employeeA = Employees.FirstShop.Employee_3.name; + + await homeNavigation.gotoModule('目标'); + await page.getByRole('button', { name: '设置目标' }).click(); + await page.locator('.popupComTableStyle .m-table__fixed-left tbody tr td:first-child').first().waitFor(); + // 员工A内行数 + const allTrA = page.locator('.popupComTableStyle .m-table__fixed-left tbody tr td:first-child'); + const nowRowA = await allTrA + .allInnerTexts() + .then(text => { + return text.findIndex(item => item.includes(employeeA)); + }) + .then(Number); + if (nowRowA === -1) { + throw new Error('员工A不存在'); + } + + await page + .locator('.popupComTableStyle .m-table__body .main-table-body_tr') + .nth(nowRowA) + .locator('.m-table-cell') + .nth(1) + .click(); + await numberInput.delAllValue(); + await numberInput.setValue(goal); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await page.locator('.ant-message', { hasText: '设置成功' }).waitFor(); + // 等待加载完成 + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + // 员工A行数 + const allTrB = page.locator('.tableStyle .m-table__fixed-left tbody tr'); + const nowRowB = await allTrB + .allInnerTexts() + .then(text => { + return text.findIndex(item => item.includes(employeeA)); + }) + .then(Number); + + const verify = page + .locator('.tableStyle .m-table__body-wrapper tbody tr') + .nth(nowRowB) + .locator('.is-right .targetValue') + .nth(0); + // 对比输入的目标 + await expect(verify).toContainText(`${goal}`); + + // 清理 + await page.getByRole('button', { name: '设置目标' }).click(); + + await page + .locator('.popupComTableStyle .m-table__body .main-table-body_tr') + .nth(nowRowA) + .locator('.m-table-cell') + .nth(1) + .click(); + await numberInput.delAllValue(); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /保\s存/ }).click(); + }); +}); + +test.describe('个人看板', () => { + test('查看个人看板', async ({ page, createCustomer, homeNavigation }) => { + // 今日消费之前和之后的现金和消耗 + let lastCash; + let lastExpend; + // 上月和本月的现金和消耗 + let lastMonthCash; + let lastMonthExpend; + + // 定义会员 + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + // 定位器 + const verify = page.locator('.m-table__header-wrapper .date_box'); + + const customer = createCustomer; + + await test.step('进入目标界面获取初始数据', async () => { + // 进入目标界面获取初始数据 + await homeNavigation.gotoModule('目标'); + await page.locator('.m-table__fixed-left .employeeName', { hasText: '张伟' }).click(); + await page.locator('.m-table__body-wrapper').last().waitFor(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + lastCash = KeepOnlyNumbers( + await verify + .filter({ has: page.locator('.title', { hasText: '现金' }) }) + .locator('.target_txt') + .last() + .innerText(), + ); + lastExpend = KeepOnlyNumbers( + await verify + .filter({ has: page.locator('.title', { hasText: '消耗' }) }) + .locator('.target_txt') + .last() + .innerText(), + ); + // 点击上个月 + await page.locator('.left_btn').last().click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + lastMonthCash = KeepOnlyNumbers( + await verify + .filter({ has: page.locator('.title', { hasText: '现金' }) }) + .locator('.target_txt') + .last() + .innerText(), + ); + lastMonthExpend = KeepOnlyNumbers( + await verify + .filter({ has: page.locator('.title', { hasText: '消耗' }) }) + .locator('.target_txt') + .last() + .innerText(), + ); + // 关闭目标窗口 + await page.locator('.close_icon').last().click(); + }); + + await test.step('购买项目A并消耗(本月)', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '张伟' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗添加员工 + await page.locator('.use_list .staff_btn').click(); + await page.locator('.check_row', { hasText: '张伟' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 选择现金结算 + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('购买项目A并消耗(补录上月)', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '张伟' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗添加员工 + await page.locator('.use_list .staff_btn').click(); + await page.locator('.check_row', { hasText: '张伟' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + // 点击补录 + await page.locator('.recording_checkbox').click(); + // 点击上个月 + await page.locator('.ant-calendar-prev-month-btn').waitFor(); + await page.locator('.ant-calendar-prev-month-btn').click(); + // 选择上个月15号 + await page.locator('.ant-calendar-cell', { hasText: '15' }).click(); + await page.locator('.date_text .date', { hasText: '15' }).waitFor(); + + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('最后目标界面校验数据', async () => { + // 进入目标界面 + await homeNavigation.gotoModule('目标'); + await page.locator('.m-table__fixed-left .employeeName', { hasText: '张伟' }).click(); + await page.locator('.m-table__body-wrapper').last().waitFor(); + await expect.soft(page.locator('.m-table__icon__warp')).toBeHidden(); + + await expect + .soft( + verify + .filter({ has: page.locator('.title', { hasText: '现金' }) }) + .locator('.target_txt') + .last(), + ) + .toContainText(`${Number(lastCash) + 50}`); + + await expect( + verify + .filter({ has: page.locator('.title', { hasText: '消耗' }) }) + .locator('.target_txt') + .last(), + ).toContainText(`${Number(lastExpend) + 60}`); + + // 点击上个月 + await page.locator('.left_btn').last().click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + await expect + .soft( + verify + .filter({ has: page.locator('.title', { hasText: '现金' }) }) + .locator('.target_txt') + .last(), + ) + .toContainText(`${Number(lastMonthCash) + 50}`); + await expect( + verify + .filter({ has: page.locator('.title', { hasText: '消耗' }) }) + .locator('.target_txt') + .last(), + ).toContainText(`${Number(lastMonthExpend) + 60}`); + }); + }); +}); + +test.describe('个人日结单', () => { + test('查看个人日结单', async ({ page, createCustomer, homeNavigation }) => { + let cash; + let expend; + + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + + const customer = createCustomer; + + await test.step('购买项目A并消耗', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '赵军' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗添加员工 + await page.locator('.use_list .staff_btn').click(); + await page.locator('.check_row', { hasText: '赵军' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 选择现金结算 + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('进入流水获取赵军今日现金及消耗', async () => { + await homeNavigation.gotoModule('流水'); + await page.locator('.tab_item', { hasText: '日结单' }).click(); + await expect(page.locator('.m-table__empty')).not.toBeVisible(); + await expect(page.locator('.m-table__loading')).toBeHidden(); + await expect(page.locator('.m-table__body-wrapper tbody tr').first()).toBeVisible(); + // 获取员工赵军在第几列 + const allTrLine = page.locator('.m-table__header-wrapper th'); + const nowLine = await allTrLine.allInnerTexts().then(text => { + return text.findIndex(item => item.includes('赵军')); + }); + + // 获取流水里赵军的今日现金及消耗 + const codes = page.locator('.m-table__body-wrapper tbody tr'); + cash = KeepOnlyNumbers( + await codes + .filter({ has: page.locator('td', { hasText: '现金' }) }) + .locator('td') + .nth(nowLine) + .innerText(), + ); + expend = KeepOnlyNumbers( + await codes + .filter({ has: page.locator('td', { hasText: '消耗' }) }) + .locator('td') + .nth(nowLine) + .innerText(), + ); + }); + + await test.step('进入目标获取赵军今日现金及消耗', async () => { + await homeNavigation.gotoModule('目标'); + // 点击赵军进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '赵军' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + const coded = page.locator('.m-table__body-wrapper .main-table-body_tr'); + await expect( + coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1), + ).toContainText(cash); + + await expect( + coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(3), + ).toContainText(expend); + }); + }); +}); + +test.describe('门店看板', () => { + test('查看门店看板', async ({ page, createCustomers, homeNavigation }) => { + // E1 李娜 + // E2 杨雪 + // E3 赵伟 + // F1 张凯 + // 总部账号:17770720220 a123456 + // 门店一 : 17770720221 a123456 + // 门店二: 17770720222 a123456 + // 组长李娜:139876543 a123456 + let customer_a; + let customer_b; + let customer_c; + let coded; + // 各员工初始数据 + let LNCash; //李娜 + let LastLNCash; //李娜上月 + let YXCash; //杨雪 + let ZWCash; //赵伟 + let ZKCash; //张凯 + + // 收银结算button + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + + /**@type {Customer[]} */ + let customers = []; + await test.step('创建3个顾客', async () => { + customers = await createCustomers(3); + }); + customer_a = customers[0]; + customer_b = customers[1]; + customer_c = customers[2]; + + await test.step('总部账号获取各门店各部门各员工数据', async () => { + // 进入目标界面获取初始数据 + await homeNavigation.gotoModule('目标'); + // 点击李娜进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '李娜' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + coded = page.locator('.m-table__body-wrapper .main-table-body_tr'); + LNCash = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + // 点击上个月 + await page.locator('.left_btn').last().click(); + // await expect (coded.filter({has:page.locator('.is-center').last()}).locator('td .kpiType').first()).not.toBeEmpty() + await page.waitForTimeout(2000); + + // 点击李娜进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '李娜' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取上月的现金以及消耗 + LastLNCash = await coded + .filter({ has: page.locator('.is-center', { hasText: '15' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + // 返回下个月以便校验其他员工数据 + await page.locator('.right_btn').last().click(); + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 点击杨雪进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '杨雪' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + YXCash = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + + // 点击选择门店 + await expect(async () => { + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 选择部门 + await page.locator('.needsclick', { hasText: '医美部' }).click(); + await page.locator('.shop_btn', { hasText: /^保\s存$/ }).click(); + await page + .locator('.m-table__fixed-left tbody .employeeName', { hasText: '赵伟' }) + .waitFor({ timeout: 2000 }); + }).toPass(); + + // 点击赵伟进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '赵伟' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + ZWCash = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + + // 点击选择门店 + await expect(async () => { + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 选择门店 + await page.locator('.shopSelect_shop_content').click(); + await page.locator('.comSelect_title', { hasText: '选择' }).waitFor(); + await page.locator('.customWith', { hasText: 'AT测试二店' }).click(); + await page.locator('.shopSelect_shop_content', { hasText: 'AT测试二店' }).waitFor(); + await page.locator('.shop_btn', { hasText: /^保\s存$/ }).click(); + await page + .locator('.m-table__fixed-left tbody .employeeName', { hasText: '张凯' }) + .waitFor({ timeout: 2000 }); + }).toPass(); + + // 点击张凯进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '张凯' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + ZKCash = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + + // 点击选择门店 + await expect(async () => { + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 选择门店 + await page.locator('.shopSelect_shop_content').click(); + await page.locator('.comSelect_title', { hasText: '选择' }).waitFor(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + await page.locator('.shopSelect_shop_content', { hasText: 'AT测试一店' }).waitFor(); + await page.locator('.shop_btn', { hasText: /^保\s存$/ }).click(); + await page + .locator('.m-table__fixed-left tbody .employeeName', { hasText: '李娜' }) + .waitFor({ timeout: 2000 }); + }).toPass(); + }); + + await test.step('顾客A购买项目A 补录上月', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer_a.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer_a.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '李娜' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + // 点击补录 + await page.locator('.recording_checkbox').click(); + // 点击上个月 + await page.locator('.ant-calendar-prev-month-btn').waitFor(); + await page.locator('.ant-calendar-prev-month-btn').click(); + // 选择上个月15号 + await page.locator('.ant-calendar-cell', { hasText: '15' }).click(); + await page.locator('.date_text .date', { hasText: '15' }).waitFor(); + + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('顾客B购买项目A 选顾问', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer_b.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer_b.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '李娜' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('顾客B购买项目A 选美容师', async () => { + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer_b.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer_b.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '杨雪' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('顾客B购买项目B 选医美部', async () => { + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer_b.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer_b.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100029' }).click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.ant-radio-button-wrapper', { hasText: '医美部' }).click(); + await page.locator('.check_row', { hasText: '赵伟' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('二店顾客C购买项目A,选二店员工', async () => { + // 点击选择门店 + await page.locator('.shop_select').click(); + await page.locator('.comSelect_title', { hasText: '选择' }).waitFor(); + // 选择二店 + await page.locator('.check_box_container', { hasText: 'AT测试二店' }).click(); + + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer_c.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer_c.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '张凯' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('总部账号验证各门店各部门各员工数据', async () => { + // 进入目标界面核对数据 + await homeNavigation.gotoModule('目标'); + // 点击选择门店 + await expect(async () => { + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 选择门店 + await page.locator('.shopSelect_shop_content').click(); + await page.locator('.comSelect_title', { hasText: '选择' }).waitFor(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + await page.locator('.shopSelect_shop_content', { hasText: 'AT测试一店' }).waitFor(); + await page.locator('.shop_btn', { hasText: /^保\s存$/ }).click(); + await page + .locator('.m-table__fixed-left tbody .employeeName', { hasText: '李娜' }) + .waitFor({ timeout: 2000 }); + }).toPass(); + + // 点击李娜进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '李娜' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + + const LNCashed = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + + // 点击杨雪进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '杨雪' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + const YXCashed = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + + // 点击选择门店 + await expect(async () => { + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 选择部门 + await page.locator('.needsclick', { hasText: '医美部' }).click(); + await page.locator('.shop_btn', { hasText: /^保\s存$/ }).click(); + await page + .locator('.m-table__fixed-left tbody .employeeName', { hasText: '赵伟' }) + .waitFor({ timeout: 2000 }); + }).toPass(); + + // 点击赵伟进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '赵伟' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + const ZWCashed = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + + // 点击选择门店 + await expect(async () => { + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 选择门店 + await page.locator('.shopSelect_shop_content').click(); + await page.locator('.comSelect_title', { hasText: '选择' }).waitFor(); + await page.locator('.customWith', { hasText: 'AT测试二店' }).click(); + await page.locator('.shopSelect_shop_content', { hasText: 'AT测试二店' }).waitFor(); + await page.locator('.shop_btn', { hasText: /^保\s存$/ }).click(); + await page + .locator('.m-table__fixed-left tbody .employeeName', { hasText: '张凯' }) + .waitFor({ timeout: 2000 }); + }).toPass(); + + // 点击张凯进入个人目标 + await page.locator('.m-table__fixed-left tbody .employeeName', { hasText: '张凯' }).click(); + await Promise.any([ + page.locator('.no_done').first().waitFor({ timeout: 5000 }), + page.locator('.no_start').first().waitFor({ timeout: 5000 }), + ]); + // 获取今日的现金以及消耗 + const ZKCashed = await coded + .filter({ has: page.locator('.is-center', { hasText: '今' }) }) + .locator('td') + .nth(1) + .innerText(); + // 关闭窗口 + await page.locator('.close_icon').last().click(); + // 点击选择门店 + await expect(async () => { + await page.locator('.shop-picker-store').click(); + await page.locator('.shopSelect_title', { hasText: '选择查询对象' }).waitFor(); + // 选择门店 + await page.locator('.shopSelect_shop_content').click(); + await page.locator('.comSelect_title', { hasText: '选择' }).waitFor(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + await page.locator('.shopSelect_shop_content', { hasText: 'AT测试一店' }).waitFor(); + await page.locator('.shop_btn', { hasText: /^保\s存$/ }).click(); + await page + .locator('.m-table__fixed-left tbody .employeeName', { hasText: '李娜' }) + .waitFor({ timeout: 2000 }); + }).toPass(); + + // 校验对比 + expect(LNCashed * 1).toBe(LNCash * 1 + 50); + expect(YXCashed * 1).toBe(YXCash * 1 + 50); + expect(ZWCashed * 1).toBe(ZWCash * 1 + 100); + expect(ZKCashed * 1).toBe(ZKCash * 1 + 50); + }); + }); + + test('展示KPI配置', async ({ page, homeNavigation }) => { + const goal = page.locator('.ant-checkbox-group .ant-col .ant-checkbox-wrapper'); + const check = goal + .filter({ has: page.locator('.needsclick', { hasText: '划卡' }) }) + .locator('.ant-checkbox-checked'); + const title = page.locator('.m-table__header-wrapper thead th', { hasText: '划卡' }); + + await test.step('进入设置确认划卡处于勾选状态', async () => { + // 进入设置 + await homeNavigation.gotoModule('设置'); + await page.locator('.tab_item', { hasText: '目标' }).click(); + await page.locator('.tag_content_title', { hasText: '目标维度' }).waitFor(); + await expect(async () => { + await goal + .filter({ has: page.locator('.needsclick', { hasText: '划卡' }) }) + .locator('.ant-checkbox') + .click(); + await expect(check).toBeVisible({ timeout: 2000 }); + }).toPass(); + }); + + await test.step('进入目标查看划卡列', async () => { + // 进入目标 + await homeNavigation.gotoModule('目标'); + await page.locator('.summary').first().waitFor(); + // 判断划卡存在 + await expect(title).toBeVisible(); + }); + + await test.step('进入设置关闭划卡', async () => { + // 进入设置 + await homeNavigation.gotoModule('设置'); + await page.locator('.tab_item', { hasText: '目标' }).click(); + await page.locator('.tag_content_title', { hasText: '目标维度' }).waitFor(); + await goal + .filter({ has: page.locator('.needsclick', { hasText: '划卡' }) }) + .locator('.ant-checkbox') + .click(); + await expect(check).not.toBeVisible(); + }); + + await test.step('判断目标不存在划卡', async () => { + // 进入目标 + await homeNavigation.gotoModule('目标'); + await page.locator('.summary').first().waitFor(); + await expect(title).not.toBeVisible(); + }); + + await test.step('重复以上操作恢复设置', async () => { + // 进入设置 + await homeNavigation.gotoModule('设置'); + await page.locator('.tab_item', { hasText: '目标' }).click(); + await page.locator('.tag_content_title', { hasText: '目标维度' }).waitFor(); + await goal + .filter({ has: page.locator('.needsclick', { hasText: '划卡' }) }) + .locator('.ant-checkbox') + .click(); + await expect(check).toBeVisible(); + + // 进入目标 + await homeNavigation.gotoModule('目标'); + await page.locator('.summary').first().waitFor(); + await expect(title).toBeVisible(); + }); + }); +}); + +test.describe('指标看板', () => { + test('查看指标看板', async ({ page, createCustomer, homeNavigation }) => { + // 定义初始获取的数据 + let cash; + let expend; + let LastMonthCash; + let LastMonthExpend; + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + // 获取员工定位器 + const $$employeeHeader = page.locator('.kpiManagement .m-table__header-wrapper tr th'); + const $$valueBody = page.locator('.kpiManagement .m-table__body-wrapper').last().locator('tbody').first(); + + const customer = createCustomer; + + /**@type {number} 员工所在列 */ + let nowColumn; + // 今天在第几行 + let todayLine = (new Date().getDate() - 1) * 3; + // 15号在第几行 + let fifteenLine = (15 - 1) * 3; + + await test.step('进入目标界面获取初始数据', async () => { + // 进入目标界面获取初始数据 + await homeNavigation.gotoModule('目标'); + + // 进入现金KPI + await page.locator('.kpiTh', { hasText: '现金' }).click(); + await page.locator('.stripe_odd').first().waitFor(); + // 获取员工所在列 + nowColumn = await $$employeeHeader.allInnerTexts().then(text => { + return text.findIndex(item => item === '王芳'); + }); + + if (nowColumn === -1) { + throw new Error('员工王芳不存在'); + } + + // 获取初始现金 + cash = await $$valueBody.locator('tr').nth(todayLine).locator('td').nth(nowColumn).innerText(); + + // 点击上个月,获取包含 '15' 的行索引 + await Promise.all([ + page.locator('.left_btn').last().click(), + page.waitForResponse(res => res.url().includes('/user_kpi') && res.status() === 200), + ]); + + // 等待上个月最后一天的日差额加载出来 + await page + .locator('.m-table__body-wrapper') + .last() + .locator('tbody') + .first() + .locator('tr') + .last() + .locator('.kpiType', { hasText: '日差额' }) + .waitFor(); + + // 获取上个月初始现金 + LastMonthCash = await $$valueBody.locator('tr').nth(fifteenLine).locator('td').nth(nowColumn).innerText(); + // 关闭现金窗口 + await page.locator('.close').last().click(); + + // 进入消耗KPI,获取初始消耗 + await page.locator('.kpiTh', { hasText: '消耗' }).click(); + await page.locator('.stripe_odd').first().waitFor(); + expend = await $$valueBody.locator('tr').nth(todayLine).locator('td').nth(nowColumn).innerText(); + + // 点击上个月,获取上个月初始消耗 + await Promise.all([ + page.locator('.left_btn').last().click(), + page.waitForResponse(res => res.url().includes('/user_kpi') && res.status() === 200), + ]); + + // 等待上个月最后一天的日差额加载出来 + await page + .locator('.m-table__body-wrapper') + .last() + .locator('tbody') + .first() + .locator('tr') + .last() + .locator('.kpiType', { hasText: '日差额' }) + .waitFor(); + + // 关闭消耗窗口 + await page.locator('.close').last().click(); + }); + + await test.step('购买项目A并消耗(补录上月)', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '王芳' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗添加员工 + await page.locator('.use_list .staff_btn').click(); + await page.locator('.check_row', { hasText: '王芳' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 点击补录 + await page.locator('.recording_checkbox').click(); + // 点击上个月 + await page.locator('.ant-calendar-prev-month-btn').waitFor(); + await page.locator('.ant-calendar-prev-month-btn').click(); + // 选择上个月15号 + await page.locator('.ant-calendar-cell', { hasText: '15' }).click(); + await page.locator('.date_text .date', { hasText: '15' }).waitFor(); + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('购买项目A并消耗(本月)', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: '100028' }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: '王芳' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗添加员工 + await page.locator('.use_list .staff_btn').click(); + await page.locator('.check_row', { hasText: '王芳' }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 选择现金结算 + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('最后目标界面校验数据', async () => { + // 进入目标界面 + await homeNavigation.gotoModule('目标'); + // 进入现金KPI + await page.locator('.kpiTh', { hasText: '现金' }).click(); + await page.locator('.stripe_odd').first().waitFor(); + + // 获取王芳今天的现金值 + const nowCash = $$valueBody.locator('tr').nth(todayLine).locator('td').nth(nowColumn); + await expect(nowCash).toContainText(`${Number(cash) + 50}`); + + // 点击上个月 + await Promise.all([ + page.locator('.left_btn').last().click(), + page.waitForResponse(res => res.url().includes('/user_kpi') && res.status() === 200), + ]); + // 等待上个月最后一天的日差额加载出来 + await page + .locator('.m-table__body-wrapper') + .last() + .locator('tbody') + .first() + .locator('tr') + .last() + .locator('.kpiType', { hasText: '日差额' }) + .waitFor(); + + // 获取上个月现金 + const nowLastMonthCash = $$valueBody.locator('tr').nth(fifteenLine).locator('td').nth(nowColumn); + await expect(nowLastMonthCash).toContainText(`${Number(LastMonthCash) + 50}`); + // 关闭现金窗口 + await page.locator('.close').last().click(); + + // 进入消耗KPI + await page.locator('.kpiTh', { hasText: '消耗' }).click(); + await page.locator('.stripe_odd').first().waitFor(); + // 获取消耗值 + const nowExpend = $$valueBody.locator('tr').nth(todayLine).locator('td').nth(nowColumn); + await expect(nowExpend).toContainText(`${Number(expend) + 60}`); + + // 点击上个月 + await Promise.all([ + page.locator('.left_btn').last().click(), + page.waitForResponse(res => res.url().includes('/user_kpi') && res.status() === 200), + ]); + + // 等待上个月最后一天的日差额加载出来 + await page + .locator('.m-table__body-wrapper') + .last() + .locator('tbody') + .first() + .locator('tr') + .last() + .locator('.kpiType', { hasText: '日差额' }) + .waitFor(); + + // 获取上个月消耗 + const nowLastMonthExpend = $$valueBody.locator('tr').nth(fifteenLine).locator('td').nth(nowColumn); + await expect(nowLastMonthExpend).toContainText(`${Number(LastMonthExpend) + 60}`); + }); + }); +}); + +test.describe('精细目标', () => { + test('设置精细化目标', async ({ page, homeNavigation, createCustomCustomer, numberInput }) => { + const project = { no: '100012', name: '雪肌晶纯护理', Price: 300 }; + const employee = staffData.firstStore.firstSector.employee_8; + + const customer = new Customer(1, 1, { employees: [{ level: '医生', name: employee.name }] }); + + let firstStoreFirstSector = staffData.firstStore.firstSector; + + await test.step('新建有员工的顾客', async () => { + await createCustomCustomer(customer); + }); + + await test.step('给员工设置目标', async () => { + await homeNavigation.gotoModule('目标'); + // 设置目标 + await page.getByRole('button', { name: '设置目标' }).click(); + // 选择部门 + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: firstStoreFirstSector.name }).click(); + // 弹窗定位器 + const popupContent = page.locator('.popup_content .m-table__body-wrapper'); + // 判断员工是否存在 + await expect(popupContent).toContainText(employee.name); + // 选择指定员工的现金列 + await popupContent.locator('tbody > tr', { hasText: employee.name }).locator('td').nth(1).click(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /保\s存/ }).click(); + }); + + await test.step('给顾客设置服务项目,查看设置的服务项目', async () => { + // 进入精细化管理页面 + await page.locator('.left_view .tab_item', { hasText: '精细化管理' }).click(); + await page.getByRole('cell', { name: '顾客' }).waitFor(); + + // 进入设置目标页面 + await page.getByRole('button', { name: '设置目标' }).click(); + await Promise.all([ + await page.locator('.com_picker .label', { hasText: employee.name }).click(), + await page.waitForResponse(res => res.url().includes('/user_kpi_detail') && res.status() === 200), + ]); + + // 设置精细化窗口定位器 + const settingPointViewLocator = page.locator('.flow_content'); + // 设置顾客定位器 + const settingCustomerLocator = settingPointViewLocator.locator( + '.m-table__body-wrapper .main-table-body_tr', + ); + + // 查询指定顾客 + const userTr = settingCustomerLocator.filter({ hasText: customer.username }); + await page.locator('.filter_box').getByRole('button').click(); + await page.getByPlaceholder('输入顾客姓名、手机号、首字母搜索').fill(customer.phone); + await Promise.all([ + page.locator('.search_btn > svg').click(), + page.waitForResponse(res => res.url().includes('/user_kpi_detail') && res.status() === 200), + ]); + await expect(userTr).toBeVisible(); + + // 增加指定项目 + await userTr.locator('td', { hasText: '增加' }).first().click(); + // 项目选择器 + const projectPickerLocator = page.locator('.com_picker', { + has: page.locator('.comSelect_title', { hasText: '添加项目' }), + }); + await projectPickerLocator.locator('.search_box input').fill(project.no); + await projectPickerLocator.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await projectPickerLocator.getByLabel(project.name).uncheck(); + await projectPickerLocator.getByLabel(project.name).check(); + await expect( + page.locator('.item_modal_tab .left_menu-item', { hasText: '全部' }).locator('.menu-item-dot'), + ).toBeVisible({ timeout: 2000 }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + await expect(userTr).toContainText(project.name); + + // 关闭设置目标页面 + await page.locator('.close_btn > .icon > svg').click(); + // 判断设置的项目状态 + await expect(page.locator('.main-table-body_tr', { hasText: customer.username }).first()).toContainText( + project.name, + ); + }); + }); + + test('完成精细目标', async ({ page, homeNavigation, createCustomCustomer, numberInput }) => { + const project = { no: '100012', name: '雪肌晶纯护理', Price: 300 }; + const employee = staffData.firstStore.firstSector.employee_8; + const firstStoreFirstSector = staffData.firstStore.firstSector; + + const customer = new Customer(1, 1, { employees: [{ level: '医生', name: employee.name }] }); + + await test.step('新建有员工的顾客', async () => { + await createCustomCustomer(customer); + }); + + await test.step('给员工设置目标', async () => { + await homeNavigation.gotoModule('目标'); + // 设置目标 + await page.getByRole('button', { name: '设置目标' }).click(); + // 选择部门 + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: firstStoreFirstSector.name }).click(); + // 弹窗定位器 + const popupContent = page.locator('.popup_content .m-table__body-wrapper'); + // 判断员工是否存在 + await expect(popupContent).toContainText(employee.name); + // 选择指定员工的现金列 + await popupContent.locator('tbody > tr', { hasText: employee.name }).locator('td').nth(1).click(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /保\s存/ }).click(); + }); + + await test.step('给顾客设置服务项目,查看设置的服务项目, 查看项目的状态', async () => { + // 进入精细化管理页面 + await page.locator('.left_view .tab_item', { hasText: '精细化管理' }).click(); + await page.getByRole('cell', { name: '顾客' }).waitFor(); + + // 进入设置目标页面 + await page.getByRole('button', { name: '设置目标' }).click(); + await Promise.all([ + await page.locator('.com_picker .label', { hasText: employee.name }).click(), + await page.waitForResponse(res => res.url().includes('/user_kpi_detail') && res.status() === 200), + ]); + + // 设置精细化窗口定位器 + const settingPointViewLocator = page.locator('.flow_content'); + // 设置顾客定位器 + const settingCustomerLocator = settingPointViewLocator.locator( + '.m-table__body-wrapper .main-table-body_tr', + ); + + // 查询指定顾客 + const userTr = settingCustomerLocator.filter({ hasText: customer.username }); + await page.locator('.filter_box').getByRole('button').click(); + await page.getByPlaceholder('输入顾客姓名、手机号、首字母搜索').fill(customer.phone); + await Promise.all([ + page.locator('.search_btn > svg').click(), + page.waitForResponse(res => res.url().includes('/user_kpi_detail') && res.status() === 200), + ]); + await expect(userTr).toBeVisible(); + + // 增加指定项目 + await userTr.locator('td', { hasText: '增加' }).first().click(); + // 项目选择器 + const projectPickerLocator = page.locator('.com_picker', { + has: page.locator('.comSelect_title', { hasText: '添加项目' }), + }); + await projectPickerLocator.locator('.search_box input').fill(project.no); + await projectPickerLocator.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await projectPickerLocator.getByLabel(project.name).uncheck(); + await projectPickerLocator.getByLabel(project.name).check(); + await expect( + page.locator('.item_modal_tab .left_menu-item', { hasText: '全部' }).locator('.menu-item-dot'), + ).toBeVisible({ timeout: 2000 }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + await expect(userTr).toContainText(project.name); + + // 等待设置项目后的页面 + await Promise.all([ + page.locator('.close_btn > .icon > svg').click(), + page.waitForResponse(res => res.url().includes('/user_kpi_detail') && res.status() === 200), + ]); + }); + + await test.step('选择顾客去开单,购买目标项目,选择目标员工结算', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + + // 选择项目 + await page.locator('.project_list .number', { hasText: project.no }).click(); + + // 选择添加员工 + await page.locator('button.staff_btn').click(); + await page.locator('.check_row', { hasText: employee.name }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.locator('.pay_btn', { hasText: /^结\s算$/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('进入目标页面,查看目标项目状态', async () => { + await homeNavigation.gotoModule('目标'); + + await page.locator('.left_view .tab_item', { hasText: '精细化管理' }).click(); + + await page.waitForLoadState('domcontentloaded'); + + const customerTr = page + .locator('#app tr') + .filter({ has: page.locator('td', { hasText: customer.username }) }); + + // 打开现金列tip + await customerTr.locator('.service_box .item_li use').first().click(); + // 判断提示窗口内容 + await expect(page.getByRole('tooltip')).toContainText('已经完成'); + }); + }); + + test('完成惊喜业绩', async ({ page, homeNavigation, createCustomers, numberInput }) => { + const project = { no: '100012', name: '雪肌晶纯护理', Price: 300 }; + const employee = staffData.firstStore.firstSector.employee_8; + + let firstStoreFirstSector = staffData.firstStore.firstSector; + let firstStoreSecondSector = staffData.firstStore.secondSector; + + /**@type {Customer[]} */ + let customers = []; + await test.step('创建两个顾客', async () => { + customers = await createCustomers(2); + }); + const customerA = customers[0]; + const customerB = customers[1]; + + await test.step('给顾客分配员工', async () => { + await homeNavigation.gotoModule('顾客'); + + // 根据手机号进行搜索,进入顾客详情页面 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customerA.phone); + await page.locator('.ant-input-suffix .search_btn', { hasText: '搜索' }).click(); + await page.locator('.custom_content', { hasText: customerA.phone }).click(); + await page.locator('.m-table__fixed-left').getByText(customerA.username).first().click(); + + // 分配员工 + await page.locator('.person_content').waitFor(); + await page.locator('.user_name').locator('i').click(); + + // 打开员工分配页面 + await page.locator('.member_info_list', { hasText: '顾问' }).locator('.beautician_box').click(); + + // 选择分配的员工 + await page.getByRole('treeitem', { name: employee.name }).click(); + + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + // 返回顾客模块 + await page.locator('.close_icons > svg').click(); + }); + + await test.step('给员工设置目标', async () => { + await homeNavigation.gotoModule('目标'); + await page.getByRole('button', { name: '设置目标' }).click(); + + const department = await page.getByRole('combobox').innerText(); + if (customerA.department === 1) { + if (department !== firstStoreFirstSector.name) { + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: firstStoreFirstSector.name }).click(); + } + } else if (customerA.department === 2) { + if (department !== firstStoreSecondSector.name) { + await page.getByRole('combobox').click(); + await page + .getByRole('option', { + name: firstStoreSecondSector.name, + }) + .click(); + } + } + + const popupContent = page.locator('.popup_content .m-table__body-wrapper'); + await expect(popupContent).toContainText(employee.name); + + // 选择某个员工的现金列 + await popupContent.locator('tbody > tr', { hasText: employee.name }).locator('td').nth(1).click(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /保\s存/ }).click(); + }); + + await test.step('给顾客设置服务项目,查看设置的服务项目, 查看项目的状态', async () => { + // await page.pause(); + await page.locator('.left_view .tab_item', { hasText: '精细化管理' }).click(); + await page.getByRole('cell', { name: '顾客' }).waitFor(); + + await page.getByRole('button', { name: '设置目标' }).click(); + await Promise.all([ + await page.locator('.com_picker .label', { hasText: employee.name }).click(), + await page.waitForResponse(res => res.url().includes('/user_kpi_detail') && res.status() === 200), + ]); + + // 设置精细化窗口 + const settingPointViewLocator = page.locator('.flow_content'); + // 设置顾客行 + const settingCustomerLocator = settingPointViewLocator.locator( + '.m-table__body-wrapper .main-table-body_tr', + ); + + const userTr = settingCustomerLocator.filter({ hasText: customerA.username }); + + await page.locator('.filter_box').getByRole('button').click(); + await page.getByPlaceholder('输入顾客姓名、手机号、首字母搜索').fill(customerA.phone); + + // 查询指定顾客 + await Promise.all([ + page.locator('.search_btn > svg').click(), + page.waitForResponse(res => res.url().includes('/user_kpi_detail') && res.status() === 200), + ]); + + await expect(userTr).toBeVisible(); + + await userTr.locator('td', { hasText: '增加' }).first().click(); + await page.waitForTimeout(2000); + // 项目选择器 + const projectPickerLocator = page.locator('.com_picker', { + has: page.locator('.comSelect_title', { hasText: '添加项目' }), + }); + await projectPickerLocator.locator('.search_box input').fill(project.no); + await projectPickerLocator.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await projectPickerLocator.locator('.label', { hasText: project.no }).click(); + await page.getByRole('button', { name: '确定选择' }).click(); + await expect(page.locator('.popup_content', { hasText: '修改次数' })).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + await expect(userTr).toContainText(project.name); + await page.locator('.close_btn > .icon > svg').click(); + }); + + await test.step('选择顾客A去开单,购买目标项目,选择目标员工结算', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customerA.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customerA.phone }).click(); + + // 选择项目 + await page.locator('.project_list .number', { hasText: project.no }).click(); + await page.getByRole('button', { name: '体' }).click(); + await page.locator('.popup_content').getByText('普通').click(); + + // 选择添加员工 + await page.locator('button.staff_btn').click(); + await page.locator('.check_row', { hasText: employee.name }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.locator('.pay_btn', { hasText: /^结\s算$/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('选择顾客B去开单,购买项目并消耗,选择目标员工结算', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customerB.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customerB.phone }).click(); + + // 购买并消耗 + await page.locator('.project_list .number', { hasText: project.no }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买添加员工 + await page.locator('#buyList .staff_btn').click(); + await page.locator('.check_row', { hasText: employee.name }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗添加员工 + await page.locator('.use_list .staff_btn').click(); + await page.locator('.check_row', { hasText: employee.name }).getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('进入目标页面,查看顾客A和顾客B的状态', async () => { + await homeNavigation.gotoModule('目标'); + await page.locator('.left_view .tab_item', { hasText: '精细化管理' }).click(); + await page.waitForLoadState('domcontentloaded'); + + // 获取顾客A的项目状态 + const customerATr = page + .locator('#app tr') + .filter({ has: page.locator('td', { hasText: customerA.username }) }); + await customerATr.locator('.service_box .item_li use').first().click(); + await expect(page.getByRole('tooltip')).toContainText('已经完成'); + // 获取顾客B的项目状态 + const customerBTr = page + .locator('#app tr') + .filter({ has: page.locator('td', { hasText: customerB.username }) }); + + await expect(customerBTr).toContainText('惊喜'); + }); + }); + + test('查看员工精细化目标', async ({ page, homeNavigation }) => { + let cashCustomer; + let cashProject; + let cashGoal; + let expendCustomer; + let expendProject; + let expendGoal; + + await test.step('获取员工目标数据', async () => { + // 进入目标页面 + await homeNavigation.gotoModule('目标'); + await page.locator('.tab_item', { hasText: '精细化管理' }).click(); + await expect.soft(page.locator('.m-table__icon__warp')).toBeHidden(); + await expect(page.locator('.custom_txt')).not.toBeVisible(); + + const code = page.locator('.m-table__body-wrapper tbody tr').first(); + // 现金顾客、项目、目标 + cashCustomer = await code.locator('td').nth(1).innerText(); + // 保留从第一个字符到x之前 + cashProject = (await code.locator('td').nth(2).innerText()).match(/^[^x]+/)?.[0]; + cashGoal = await code.locator('td').nth(3).locator('.has_done').innerText(); + // 消耗顾客、项目、目标 + expendCustomer = await code.locator('td').nth(1).innerText(); + expendProject = (await code.locator('td').nth(5).innerText()).match(/^[^x]+/)?.[0]; + expendGoal = await code.locator('td').nth(6).locator('.has_done').innerText(); + }); + + await test.step('获取员工精细化数据做对比', async () => { + // 点击员工进入员工精细化管理 + await page.locator('.m-table__fixed-left tbody tr').first().locator('td').first().click(); + await page.locator('.title', { hasText: '员工精细化目标' }).waitFor(); + await expect.soft(page.locator('.m-table__icon__warp')).toBeHidden(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 获取最后一个表格行 + const cedes = page.locator('.m-table__body-wrapper').last().locator('tr').first(); + + // 现金 + await expect.soft(cedes.locator('td').nth(0)).toContainText(cashCustomer); + await expect.soft(cedes.locator('td').nth(1)).toContainText(cashProject); + await expect.soft(cedes.locator('td').nth(2)).toContainText(cashGoal); + // 消耗 + await expect.soft(cedes.locator('td').nth(0)).toContainText(expendCustomer); + await expect.soft(cedes.locator('td').nth(4)).toContainText(expendProject); + await expect(cedes.locator('td').nth(5)).toContainText(expendGoal); + }); + }); +}); diff --git a/tests/touch/boss_inventory.spec.ts b/tests/touch/boss_inventory.spec.ts new file mode 100644 index 0000000..aa74c6f --- /dev/null +++ b/tests/touch/boss_inventory.spec.ts @@ -0,0 +1,3778 @@ +// @ts-check +import { faker } from '@faker-js/faker/locale/zh_CN'; +import { expect, test } from '@/fixtures/boss_common.js'; +import { CleanPunctuation, getListIndexForTargetElement, KeepOnlyNumbers } from '@/utils/utils.js'; +import { ProjectName } from '@/fixtures/userconfig.js'; +import { Customer } from '@/utils/customer'; + +test.describe('出入库管理', () => { + test.describe('入库单', () => { + test('入库', async ({ page, homeNavigation, numberInput }) => { + const remark = '入库' + faker.helpers.fromRegExp(/1[0-9]{4}/); + const productA = ProjectName.Product.Product_3; + + /**@type {number} 产品A余量 */ + let productASurplus; + const quantity1 = 100; + const quantity2 = 5; + await test.step('获取入库前数据', async () => { + // 进入AT测试一店的入库页面 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^入\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + + // 获取入库前的库存信息 + const $productA = page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productA.name }), + }) + .filter({ + has: page.locator('td', { hasText: productA.num }), + }); + + // 等待加载出指定内容,获取产品A的余量 + await expect($productA).toBeVisible(); + productASurplus = await $productA.locator('td').nth(4).innerText().then(Number); + + // 入库产品A,判断点击该产品有两行入库(负数才有) + await $productA.click(); + const $$productAOrder = page.locator('.bill_report .m-table__body tbody tr'); + await $$productAOrder.first().waitFor(); + expect(await $$productAOrder.count()).toBe(2); + + // 点击第一个产品修改数量 100 + await page.locator('.m-table-cell_warp').first().click(); + await numberInput.setValue(quantity1); + await numberInput.confirmValue(); + + // 点击最后一个入库 修复数量 5 + await page.locator('.m-table-cell_warp').last().click(); + await numberInput.setValue(quantity2); + await numberInput.confirmValue(); + + // 选择入库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '采购入库' }).click(); + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认入库' }).click(); + await expect(page.getByRole('button', { name: '确认入库' })).not.toBeVisible(); + }); + + /**@type {number} 入库单的索引 */ + let billEntryIndex; + await test.step('校验入库单', async () => { + // 根据备注找出入库单在第几行 + const codes = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + await expect(codes.first()).toBeInViewport(); + billEntryIndex = await codes.allInnerTexts().then(text => { + return text.findIndex(item => item.includes(remark)); + }); + + if (billEntryIndex === -1) { + throw new Error('未找到入库单'); + } + + // 判断该状态为未审核状态 + await expect(codes.nth(billEntryIndex).locator('td').nth(-6)).toContainText('未审核'); + + // 进入审核页面,确认入库,判断该状态为已审核状态 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(billEntryIndex) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + await expect(codes.nth(billEntryIndex).locator('td').nth(-6)).toContainText('已审核'); + + await page.getByRole('button', { name: /^入\s库$/ }).click(); + }); + + await test.step('反审入库单', async () => { + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + // 获取入库后的库存余量 + const $$productA = page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productA.num }), + }) + .filter({ + has: page.locator('td', { hasText: productA.name }), + }) + .locator('td'); + // 校验入库后的数量 + await expect($$productA.nth(4)).toContainText(`${productASurplus + quantity1 + quantity2}`); + // 关闭商品入库 + await page.locator('.comBill_header .close').click(); + + // 进行反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(billEntryIndex) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: /^入\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 最后校验反审后恢复原来的数量 + await expect($$productA.nth(4)).toContainText(`${productASurplus}`); + }); + }); + + test('期初入库', async ({ page, homeNavigation }) => { + // 出入库管理代码段 + const code = page.locator('.m-table__body-wrapper .main-table-body_tr').first(); + // 产品编码代码段 + const Productcode = page.locator('.upload_file tbody tr').first().locator('td'); + + await test.step('进入设置添加产品', async () => { + await homeNavigation.gotoModule('设置'); + await page.locator('.tab_item', { hasText: '库存' }).click(); + await page.locator('.tag_list_box').first().waitFor(); + // 点击下拉框 + await page.locator('.tab_item', { hasText: '库存' }).click(); + await page.locator('.ant-dropdown-open').waitFor(); + await page.locator('.ant-dropdown-menu-item', { hasText: '期初数据' }).click(); + await page.locator('.thead_tr').first().waitFor(); + // 点击产品编码,span + await Productcode.nth(1).click(); + // 输入编号和初始库存 + await Productcode.nth(1).locator('.ant-input').first().fill(ProjectName.Product.Product_5.num); + await Productcode.nth(2).locator('.ant-input').last().fill('1'); + // 确认添加 + await page.locator('.comfirm_add', { hasText: '确认添加' }).click(); + await page.locator('.popup_content .title', { hasText: '导入成功' }).waitFor(); + await page.locator('.popup_content .operation_btn .comfirm_btn', { hasText: /^确\s认$/ }).click(); + }); + + await test.step('进入库存校验', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.locator('.m-table__fixed-left th .m-table-cell', { hasText: '门店' }).waitFor(); + // 点击门店选择器 + await page.locator('.shop_select .text-ellipsis').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor(); + // 选择总部 + await expect(async () => { + await page.locator('.label_item', { hasText: '总部' }).click(); + await page.locator('.ant-checkbox-wrapper-checked', { hasText: '总部' }).waitFor({ timeout: 2000 }); + }).toPass(); + await expect(async () => { + await page.locator('.label_item', { hasText: 'AT测试一店' }).click(); + await expect( + page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' }), + ).not.toBeVisible({ timeout: 2000 }); + }).toPass(); + // 确认选择 + await page.locator('.comPicker_btn', { hasText: '确定选择' }).click(); + // 选择入库方式 + await page.locator('.ant-select-selection__rendered', { hasText: '全部入库方式' }).click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '期初导入' }).click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + // 校验 + const verify1 = code.locator('.m-table-cell', { hasText: '期初导入' }); + await expect(verify1).toBeVisible(); + const verify2 = code.locator('.property_item', { hasText: ' 肌因深层x1 ' }); + await expect(verify2).toBeVisible(); + }); + + await test.step('冻结', async () => { + await page.locator('.m-table__fixed-right .m-table-cell_btn', { hasText: '冻结' }).first().click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + await expect(code.filter({ has: page.locator('.m-table-cell', { hasText: '已冻结' }) })).toBeVisible(); + }); + + await test.step('解冻', async () => { + await page.locator('.m-table__fixed-right .m-table-cell_btn', { hasText: '解冻' }).first().click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + await expect( + code.filter({ has: page.locator('.m-table-cell', { hasText: '已冻结' }) }), + ).not.toBeVisible(); + }); + }); + + test('入库明细', async ({ page, homeNavigation, numberInput }) => { + // 入库产品 + const productA = ProjectName.Product.Product_9.name; + const productNumA = ProjectName.Product.Product_9.num; + const productB = ProjectName.Product.Product_10.name; + const productNumB = ProjectName.Product.Product_10.num; + const remark = faker.helpers.fromRegExp(/1[0-9]{10}/); + // 余量 + let lastSurplus; + + await test.step('记录一个产品', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^入\s库$/ }).click(); + await page.locator('.customWith', { hasText: '总部' }).click(); + await page.locator('.m-table__header-wrapper thead th', { hasText: '产品名称' }).first().waitFor(); + // 点击A产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNumA }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(0) + .click(); + + // 余量 + lastSurplus = await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNumA }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(4) + .innerText(); + }); + + await test.step('添加备注入库一单', async () => { + // 选择库存数量 + await page.locator('.m-table-cell_warp').first().click(); + await numberInput.setValue(2); + await numberInput.confirmValue(); + + // 选择入库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '采购入库' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认入库' }).click(); + await expect( + page.locator('.m-table__header-wrapper thead th', { hasText: '产品名称' }).first(), + ).not.toBeVisible(); + }); + + await test.step('通过备注找单据明细', async () => { + // 根据备注找出入库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 对比产品编号 + const productID = await page.locator('.bill_report .m-table-fixed-body .auto_desc').nth(0).innerText(); + expect(productID).toBe(productNumA); + + // 对比产品名称 + const product = await page.locator('.bill_report .m-table-fixed-body .auto_desc').nth(1).innerText(); + expect(product).toBe(productA); + + // 对比余量 + const surplus = await page.locator('.bill_report .main-table-body_tr td').nth(3).innerText(); + expect(surplus).toBe(lastSurplus); + + // 对比入库数量 + const quantity = await page.locator('.bill_report .main-table-body_tr td').nth(4).innerText(); + expect(quantity).toBe('2'); + }); + + await test.step('修改入库单', async () => { + // 点击三个点 + await page.locator('.more_btn').click(); + // 修改 + await page.locator('.ant-dropdown-menu-item', { hasText: '修改' }).click(); + await page.locator('.m-table__header-wrapper thead th', { hasText: '产品名称' }).first().waitFor(); + + // 选择库存数量 + await page.locator('.m-table-cell_warp').first().click(); + await numberInput.setValue(4); + await numberInput.confirmValue(); + + // 点击B产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNumB }), + }) + .filter({ + has: page.locator('td', { hasText: productB }), + }) + .locator('td') + .nth(0) + .click(); + + await page.getByRole('button', { name: '确认入库' }).click(); + await expect( + page.locator('.m-table__header-wrapper thead th', { hasText: '产品名称' }).first(), + ).not.toBeVisible(); + }); + + await test.step('通过备注再次找单据明细', async () => { + // 根据备注找出入库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + const codes = page.locator('.bill_report .main-table-body_tr'); + + // 对比A余量 + const surplus = await codes + .filter({ has: page.locator('td', { hasText: productNumA }) }) + .filter({ has: page.locator('td', { hasText: productA }) }) + .locator('td') + .nth(3) + .innerText(); + expect(surplus).toBe(lastSurplus); + + // 对比A入库数量 + const quantity = await codes + .filter({ has: page.locator('td', { hasText: productNumA }) }) + .filter({ has: page.locator('td', { hasText: productA }) }) + .locator('td') + .nth(4) + .innerText(); + expect(quantity).toBe('4'); + + // 对比B入库数量 + const quantityB = await codes + .filter({ has: page.locator('td', { hasText: productNumB }) }) + .filter({ has: page.locator('td', { hasText: productB }) }) + .locator('td') + .nth(4) + .innerText(); + expect(quantityB).toBe('1'); + }); + + await test.step('删除', async () => { + // 点击三个点 + await page.locator('.more_btn').click(); + // 删除 + await page.locator('.ant-dropdown-menu-item', { hasText: '删除' }).click(); + await page.locator('.popup_content .title', { hasText: '删除单据' }).waitFor(); + // 确认 + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + }); + + test('查询', async ({ page, homeNavigation, numberInput }) => { + // 入库产品 + const productA = ProjectName.Product.Product_9.name; + const productNumA = ProjectName.Product.Product_9.num; + const remark = faker.helpers.fromRegExp(/1[0-9]{10}/); + // 余量 + let lastSurplus; + let nowRowB = 0; + + await test.step('记录一个产品', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^入\s库$/ }).click(); + await page.locator('.customWith', { hasText: '总部' }).click(); + await page.locator('.m-table__header-wrapper thead th', { hasText: '产品名称' }).first().waitFor(); + // 点击A产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNumA }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(0) + .click(); + + // 余量 + lastSurplus = await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNumA }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(4) + .innerText(); + }); + + await test.step('添加备注入库一单', async () => { + // 选择库存数量 + await page.locator('.m-table-cell_warp').first().click(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + + // 选择入库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '采购入库' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认入库' }).click(); + await expect( + page.locator('.m-table__header-wrapper thead th', { hasText: '产品名称' }).first(), + ).not.toBeVisible(); + }); + + await test.step('通过备注找单据明细', async () => { + await page.locator('.ant-select-selection__rendered', { hasText: '审核状态' }).click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '未审核' }).click(); + // 根据备注找出入库单在第几行 + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 对比产品编号 + const productID = await page.locator('.bill_report .m-table-fixed-body .auto_desc').nth(0).innerText(); + expect(productID).toBe(productNumA); + + // 对比产品名称 + const product = await page.locator('.bill_report .m-table-fixed-body .auto_desc').nth(1).innerText(); + expect(product).toBe(productA); + + // 对比余量 + const surplus = await page.locator('.bill_report .main-table-body_tr td').nth(3).innerText(); + expect(surplus).toBe(lastSurplus); + + // 对比入库数量 + const quantity = await page.locator('.bill_report .main-table-body_tr td').nth(4).innerText(); + expect(quantity).toBe('1'); + + // 关闭窗口 + await page.locator('.close_icon').last().click(); + }); + + await test.step('选择门店,状态', async () => { + // 点击门店选择器 + await page.locator('.shop_select .text-ellipsis').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor(); + // 选择一店 + await expect(async () => { + await page.locator('.com_picker').last().locator('.label_item', { hasText: 'AT测试一店' }).click(); + await expect( + page + .locator('.com_picker') + .last() + .locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' }), + ).toBeVisible({ timeout: 2000 }); + }).toPass(); + await expect(async () => { + await page.locator('.com_picker').last().locator('.label_item', { hasText: '总部' }).click(); + await expect( + page + .locator('.com_picker') + .last() + .locator('.ant-checkbox-wrapper-checked', { hasText: '总部' }), + ).not.toBeVisible(); + }).toPass(); + + // 确认选择 + await page.locator('.comPicker_btn', { hasText: '确定选择' }).click(); + + // 判断该单不存在 + const bill = page.locator('.m-table__body-wrapper .m-table__body tbody tr td', { hasText: remark }); + await expect(bill).not.toBeVisible(); + }); + + await test.step('删除', async () => { + // 点击门店选择器 + await page.locator('.shop_select .text-ellipsis').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor(); + // 选择总部 + await expect(async () => { + await page.locator('.com_picker').last().locator('.label_item', { hasText: '总部' }).click(); + await expect( + page + .locator('.com_picker') + .last() + .locator('.ant-checkbox-wrapper-checked', { hasText: '总部' }), + ).toBeVisible(); + }).toPass(); + + await expect(async () => { + await page.locator('.com_picker').last().locator('.label_item', { hasText: 'AT测试一店' }).click(); + await expect( + page + .locator('.com_picker') + .last() + .locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' }), + ).not.toBeVisible({ timeout: 2000 }); + }).toPass(); + + // 确认选择 + await page.locator('.comPicker_btn', { hasText: '确定选择' }).click(); + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + + // 点击三个点 + await page.locator('.more_btn').click(); + // 删除 + await page.locator('.ant-dropdown-menu-item', { hasText: '删除' }).click(); + await page.locator('.popup_content .title', { hasText: '删除单据' }).waitFor(); + // 确认 + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + }); + }); + + test.describe('出库单', () => { + test('出库', async ({ page, homeNavigation, numberInput }) => { + const remark = faker.helpers.fromRegExp(/1[0-9]{10}/); + const productA = ProjectName.Product.Product_4.name; + const productNum = ProjectName.Product.Product_4.num; + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^出\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 等待加载出指定内容 + await page.locator('.panel_report .m-table__header-wrapper').waitFor(); + // 获取出库前的库存信息 + const productId = CleanPunctuation( + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(0) + .innerText(), + ); + expect(productId).toBe(productNum); + + const productName = CleanPunctuation( + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(1) + .innerText(), + ); + expect(productName).toBe(productA); + + const productSurplus = CleanPunctuation( + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(4) + .innerText(), + ); + + // 点击该产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(0) + .click(); + + // 点击产品修改数量 + await page.locator('.m-table-cell_warp').first().click(); + const quantity1 = '10'; + await numberInput.setValue(Number(quantity1)); + await numberInput.confirmValue(); + // 选择出库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '产品销售' }).click(); + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认出库' }).click(); + + // // 判断该状态为未审核状态 + const examineStatus = CleanPunctuation( + await page + .locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: remark }), + }) + .locator('td') + .nth(7) + .innerText(), + ); + expect(examineStatus).toBe('未审核'); + + // 根据备注找出库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + + // 判断该状态为已审核状态 + const examineStatusd = CleanPunctuation( + await page + .locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: remark }), + }) + .locator('td') + .nth(7) + .innerText(), + ); + expect(examineStatusd).toBe('已审核'); + + await page.getByRole('button', { name: /^出\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 获取出库后的库存余量 + const productSurplused = CleanPunctuation( + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(4) + .innerText(), + ); + expect(Number(productSurplused)).toBe(Number(productSurplus) + Number(quantity1)); + + // 重新找到该单在第几行(受其他用例影响) + let nowRowC = 0; + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowC = i; + break; + } + } + + // 关闭商品出库 + await page.locator('.comBill_header .close').click(); + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowC) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: /^出\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + const productSurpluseds = CleanPunctuation( + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(4) + .innerText(), + ); + console.log('用例结束剩余' + productSurpluseds); + console.log('用例开始剩余' + productSurplus); + expect(Number(productSurpluseds)).toBe(Number(productSurplus)); + }); + }); +}); + +test.describe('库存管理', () => { + test.describe('库存余量', () => { + test('查询', async ({ page, homeNavigation, tablePage }) => { + const goodsFromA = { + no: 'aa100016', + name: '启动青春套', + price: 2380, + isSale: true, + isConsumable: true, + }; + const goodsFromB = { + no: 'aa100019', + name: '舒眠套', + price: 0, + isSale: false, + isConsumable: true, + }; + + const $goodsFromATr = tablePage.bodyTrTable.filter({ + hasText: goodsFromA.name, + }); + const $goodsFromBTr = tablePage.bodyTrTable.filter({ + hasText: goodsFromB.name, + }); + + await test.step('根据耗材和非耗材搜索产品A', async () => { + await homeNavigation.gotoModule('库存'); + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goodsFromA.no); + await page.locator('.search_icon').first().click(); + await tablePage.bodyTrTable.first().waitFor(); + await page.getByLabel('耗材', { exact: true }).check(); + await expect.soft($goodsFromATr).toBeVisible(); + await page.getByLabel('非耗材', { exact: true }).check(); + await expect.soft($goodsFromATr).not.toBeVisible(); + }); + + await test.step('根据产品和非产品搜索产品B', async () => { + await page.getByLabel('卖品', { exact: true }).check(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('耗材', { exact: true }).uncheck(); + await page.getByLabel('非耗材', { exact: true }).uncheck(); + + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goodsFromB.no); + await page.locator('.search_icon').first().click(); + await tablePage.bodyTrTable.first().waitFor(); + await page.getByLabel('卖品', { exact: true }).uncheck(); + await expect.soft($goodsFromBTr).toBeVisible(); + + await page.getByLabel('卖品', { exact: true }).check(); + await page.getByLabel('非卖品', { exact: true }).uncheck(); + await expect.soft($goodsFromBTr).not.toBeVisible(); + }); + + await test.step('搜索产品分类', async () => { + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(''); + await page.getByLabel('卖品', { exact: true }).check(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('耗材', { exact: true }).uncheck(); + await page.getByLabel('非耗材', { exact: true }).uncheck(); + await tablePage.bodyTrTable.first().waitFor(); + + await page.getByText('分类查询').click(); + await page + .locator('.property_item') + .filter({ hasText: '产品类别' }) + .getByRole('combobox') + .filter({ hasText: '请选择内容' }) + .click(); + await page.getByRole('option', { name: '美容' }).click(); + + await page.getByRole('button', { name: /搜\s索/ }).click(); + await tablePage.bodyTrTable.first().waitFor(); + const goodsCategoryIndex = await tablePage.getFirstHeaderTableIndex('产品类别'); + + await page.getByRole('cell', { name: '产品类别' }).getByText('产品类别').click(); + await tablePage.bodyTrTable.first().waitFor(); + await expect(tablePage.bodyTrTable.first().locator('td').nth(goodsCategoryIndex)).toContainText('美容'); + + await page.getByRole('cell', { name: '产品类别' }).getByText('产品类别').click(); + await tablePage.bodyTrTable.first().waitFor(); + await expect(tablePage.bodyTrTable.first().locator('td').nth(goodsCategoryIndex)).toContainText('美容'); + + await page.getByRole('cell', { name: '产品类别' }).getByText('产品类别').click(); + await tablePage.bodyTrTable.first().waitFor(); + await expect(tablePage.bodyTrTable.first().locator('td').nth(goodsCategoryIndex)).toContainText('美容'); + }); + }); + + test('流水明细', async ({ page, homeNavigation, numberInput }) => { + //出库数量 + const quantity1 = 10; + + const remark = faker.helpers.fromRegExp(/1[0-9]{10}/); + const productA = ProjectName.Product.Product_2.name; + const productNum = ProjectName.Product.Product_2.num; + + /**@type {string} 单号*/ + let billNo; + await test.step('出库', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^出\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 点击该产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(0) + .click(); + + // 点击产品修改数量 + await page.locator('.m-table-cell_warp').first().click(); + await numberInput.setValue(quantity1); + await numberInput.confirmValue(); + // 选择出库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '产品开用' }).click(); + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认出库' }).click(); + + // 根据备注找出库单在第几行 + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const nowRowB = await allTrB.allInnerTexts().then(text => { + return text.findIndex(item => item.includes(remark)); + }); + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + + // 判断该状态为已审核状态 + await expect( + page + .locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: remark }), + }) + .locator('td') + .nth(7), + ).toContainText('已审核'); + + const billNo = CleanPunctuation( + await page + .locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: remark }), + }) + .locator('td') + .nth(1) + .innerText(), + ); + expect(billNo).not.toBe(''); + }); + + await test.step('产品余量', async () => { + await page.locator('.tab_item', { hasText: '库存管理' }).click(); + // 根据项目编号点击产品余量 + await page + .locator('.m-table-with-footer .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .locator('.on_click_cell') + .first() + .click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + const code = page.locator('.hideTop .m-table-wrapper .m-table__body-wrapper'); + if (await code.isVisible()) { + const $$billNoTr = page + .locator('.popup_content .m-table-border .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td'); + // 根据单号找到刚出库的单子 获取库存变动以及结余 + await expect($$billNoTr.nth(4)).toContainText(`${-quantity1}`); + + const code = page.locator( + '.popup_content .m-table-border .m-table__body-wrapper .m-table__body tbody tr', + ); + const nowRow = await code.allInnerTexts().then(text => { + return text.findIndex(item => item.includes(billNo)); + }); + + // 结余 + const surplus = Number(await $$billNoTr.nth(5).innerText()); + await expect( + code + .nth(nowRow + 1) + .locator('td') + .nth(5), + ).toContainText(`${surplus + quantity1}`); + // 关闭窗口 + await page.locator('.noMargin .close_icon').click(); + } else { + console.log('出库失败,该产品无出库数据'); + } + }); + + await test.step('开用余量', async () => { + // 根据项目编号点击开用余量 + await page + .locator('.m-table-with-footer .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .locator('.on_click_cell') + .last() + .click(); + + // 获取该产品的容量 + const capacity = KeepOnlyNumbers( + await page.locator('.hideTop .goodsFlow_session_rt span').nth(4).innerText(), + ); + const code = page + .locator('.hideTop .m-table-wrapper .m-table__body-wrapper .main-table-body_tr') + .first(); + + if (await code.isVisible()) { + const $$billNoTr = page + .locator('.goodsFlow_table .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td'); + // 根据单号找到刚出库的单子 获取库存变动以及结余 + await expect($$billNoTr.nth(5)).toContainText(`${quantity1 * capacity}`); + + const codeA = page.locator('.goodsFlow_table .m-table__body tbody tr'); + // 对比结余 + let nowRowA = await codeA.allInnerTexts().then(text => { + return text.findIndex(item => item.includes(billNo)); + }); + + // 结余 + const surplus = await $$billNoTr.nth(6).innerText().then(Number); + await expect( + codeA + .nth(nowRowA + 1) + .locator('td') + .nth(6), + ).toContainText(`${surplus - capacity * quantity1}`); + + // 关闭窗口 + await page.locator('.noMargin .close_icon').click(); + } else { + console.log('出库失败,该产品无出库数据'); + // 关闭窗口 + await page.locator('.noMargin .close_icon').click(); + } + }); + + await test.step('反审清理数据', async () => { + await page.reload(); + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.locator('.outInStock_filter .search_select').last().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '出库单' }).click(); + + // 根据备注找出库单在第几行 + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const nowRowC = await allTrB.allInnerTexts().then(text => { + return text.findIndex(item => item.includes(remark)); + }); + + // 反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowC) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + }); + }); + }); + + test.describe('安全库存差额表', async () => { + test('查询', async ({ page, homeNavigation, tablePage, inventoryManagementPage, numberInput }) => { + const goodsFromA = { + no: 'aa100016', + name: '启动青春套', + price: 2380, + isSale: true, + isConsumable: true, + }; + const goodsFromB = { + no: 'aa100019', + name: '舒眠套', + price: 0, + isSale: false, + isConsumable: true, + }; + + await test.step('进入安全库存差额表', async () => { + await homeNavigation.gotoModule('库存'); + await inventoryManagementPage.gotoSubPage('安全库存差额表'); + }); + + const $goodsFromATr = tablePage.bodyTrTable.filter({ + hasText: goodsFromA.name, + }); + const $goodsFromBTr = tablePage.bodyTrTable.filter({ + hasText: goodsFromB.name, + }); + + await test.step('查询产品A', async () => { + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goodsFromA.no); + await page.locator('.search_icon').first().click(); + + await page.getByLabel('卖品', { exact: true }).uncheck(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('仅显示差额产品', { exact: true }).uncheck(); + await expect.soft($goodsFromATr).not.toBeVisible(); + + await page.getByLabel('卖品', { exact: true }).check(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('仅显示差额产品', { exact: true }).uncheck(); + await expect.soft($goodsFromATr).toBeVisible(); + + await page.getByLabel('卖品', { exact: true }).uncheck(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('仅显示差额产品', { exact: true }).check(); + await expect.soft($goodsFromATr).not.toBeVisible(); + }); + + await test.step('查询产品B', async () => { + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goodsFromB.no); + await page.locator('.search_icon').first().click(); + + await page.getByLabel('卖品', { exact: true }).uncheck(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('仅显示差额产品', { exact: true }).uncheck(); + await expect.soft($goodsFromBTr).toBeVisible(); + + await page.getByLabel('卖品', { exact: true }).uncheck(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('仅显示差额产品', { exact: true }).check(); + await expect.soft($goodsFromBTr).not.toBeVisible(); + + await page.getByLabel('卖品', { exact: true }).check(); + await page.getByLabel('非卖品', { exact: true }).check(); + await page.getByLabel('仅显示差额产品', { exact: true }).uncheck(); + await expect.soft($goodsFromBTr).toBeVisible(); + }); + + await test.step('设置产品A的安全库存差额', async () => { + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goodsFromA.no); + await page.locator('.search_icon').first().click(); + + const $goodsFromA = tablePage.bodyTrTable.filter({ + hasText: goodsFromA.name, + }); + + const $goodsMargin = $goodsFromA.locator('td').nth(4); + const $safetyStock = $goodsFromA.locator('td').nth(5); + const $goodsDiff = $goodsFromA.locator('td').nth(6); + + const goodsMargin = Number(await $goodsMargin.innerText()); + + await $safetyStock.click(); + const random = faker.number.int({ min: 1, max: 100 }); + await numberInput.setValue(random); + await numberInput.confirmValue(); + await expect($goodsDiff).toContainText(`${Math.abs(goodsMargin - 100)}`); + }); + }); + }); + + test.describe('寄存余量表', async () => { + test('查询', async ({ page, homeNavigation, tablePage, inventoryManagementPage }) => { + await test.step('进入寄存余量表', async () => { + await homeNavigation.gotoModule('库存'); + await inventoryManagementPage.gotoSubPage('寄存余量表'); + }); + + const goods = { + no: 'aa100001', + name: '家居搭配护理套', + }; + const $goodsTr = tablePage.bodyTrTable + .filter({ + hasText: goods.name, + }) + .filter({ + hasText: goods.no, + }); + + await test.step('使用产品编码、产品名称进行搜索', async () => { + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goods.no); + await page.locator('.search_icon').first().click(); + await expect($goodsTr).toBeVisible(); + + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goods.name); + await page.locator('.search_icon').first().click(); + await expect($goodsTr).toBeVisible(); + }); + + await test.step('查询寄存余量排序', async () => { + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(''); + await page.locator('.search_icon').first().click(); + + const $firstTr = tablePage.bodyTrTable.first(); + const $secondTr = tablePage.bodyTrTable.nth(1); + + // 倒序 + await page.getByText('寄存余量', { exact: true }).click(); + let firstStorageMargin = Number(await $firstTr.locator('td').nth(6).innerText()); + let secondStorageMargin = Number(await $secondTr.locator('td').nth(6).innerText()); + expect.soft(secondStorageMargin).toBeGreaterThanOrEqual(firstStorageMargin); + + // 正序 + await page.getByText('寄存余量', { exact: true }).click(); + firstStorageMargin = Number(await $firstTr.locator('td').nth(6).innerText()); + secondStorageMargin = Number(await $secondTr.locator('td').nth(6).innerText()); + expect.soft(firstStorageMargin).toBeGreaterThanOrEqual(secondStorageMargin); + }); + }); + + test('寄存记录', async ({ + page, + homeNavigation, + createCustomer, + customerPage, + tablePage, + inventoryManagementPage, + }) => { + const goods = { + no: 'aa100022', + name: '芳香护理套(身体)', + }; + + const customer = createCustomer; + + /**@type {string} */ + let billNo; + await test.step('进行开单购买卖品并寄存', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + + await page.getByText('卖品', { exact: true }).click(); + + await page.locator('.project_list .number').filter({ hasText: goods.no }).click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算获取单号 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /结\s算/ }).click(), + ]); + billNo = (await response.json())?.content?.billNo; + expect(billNo).not.toBeNull(); + + // 寄存 + await page.getByRole('button', { name: '转寄存' }).click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(page.getByRole('button', { name: /开\s单/ })).toBeVisible(); + }); + + await test.step('进入寄存余量表', async () => { + await homeNavigation.gotoModule('库存'); + await inventoryManagementPage.gotoSubPage('寄存余量表'); + }); + + const $goodsTr = tablePage.bodyTrTable + .filter({ + hasText: goods.name, + }) + .filter({ + hasText: goods.no, + }); + + await test.step('搜索产品,查询寄存余量、寄存记录', async () => { + // 搜索产品 + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goods.no); + await page.locator('.search_icon').first().click(); + + await expect($goodsTr).toBeVisible(); + // 点击寄存余量 + const $goodsStorageMargin = $goodsTr.locator('td').nth(6); + await $goodsStorageMargin.click(); + + // 查看寄存余量 + const $customerTr = page.locator('.main-table-body_tr').filter({ hasText: customer.username }); + await expect.soft($customerTr.locator('td').nth(1)).toContainText('1'); + + // 查看寄存记录 + await $customerTr.getByText('寄存记录').click(); + await expect.soft(page.getByText(billNo)).toBeVisible(); + }); + }); + }); +}); + +test.describe('统计', async () => { + test.describe('入库统计', () => { + test('入库流水', async ({ page, homeNavigation, numberInput }) => { + const remark = faker.helpers.fromRegExp(/1[0-9]{10}/); + const productA = ProjectName.Product.Product_5.name; + const productNum = ProjectName.Product.Product_5.num; + + const quantity1 = 20; //数量 + const UnitPrice = 10; //单价 + /**@type {string} 单号*/ + let billNo; + let totality; + let GrossAmount; + + await test.step('获取入库前数据', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '统计' }).click(); + // 点击统计下拉框选择入库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '入库统计' }).click(); + // 选择门店 + await page.locator('.shop_picker').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor(); + await page.locator('.compicker_part-box .label', { hasText: 'AT测试一店' }).click(); + await page.locator('.compicker_part-box .label', { hasText: '总部' }).click(); + await page.getByRole('button', { name: '确定选择' }).click(); + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + await page.locator('.keyword_search input').fill(productNum); + // 点击搜索 + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + await page.locator('.ant-input-clear-icon').click(); + await page.locator('.keyword_search input').fill(productNum); + await expect(async () => { + // 点击搜索 + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + await expect(page.locator('.m-table__loading')).toBeHidden(); + }).toPass({ timeout: 60000 }); + const NotFound = page.locator('.m-table__empty', { hasText: '抱歉,未搜索到相关数据' }); + if (await NotFound.isVisible()) { + totality = 0; + GrossAmount = 0; + } else { + await page + .locator('.main-table-body_tr') + .last() + .locator('td', { hasText: productNum }) + .waitFor({ timeout: 2000 }); + + // 获取总数 + totality = KeepOnlyNumbers(await page.locator('.main-table-body_tr td').nth(5).innerText()); + console.log('前' + totality); + // 获取总额 + GrossAmount = KeepOnlyNumbers(await page.locator('.main-table-body_tr td').nth(6).innerText()); + console.log('前' + GrossAmount); + } + }); + + await test.step('入库', async () => { + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^入\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 点击该产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(0) + .click(); + // 点击设置 + await page.locator('.set_icon').click(); + // 等待弹窗出来 + await page.locator('.popup_content .content').waitFor(); + // 点击显示单价和小计 + await page.locator('.set_btn .ant-checkbox-input ').check(); + // 确认 + await page.locator('.ant-btn-lg', { hasText: /^确\s认$/ }).click(); + // 选择库存数量 + await page.locator('.m-table-cell_warp').first().click(); + await numberInput.setValue(quantity1); + await numberInput.confirmValue(); + + // 点击修改单价 + await page.locator('.m-table-cell_warp').last().click(); + await numberInput.setValue(UnitPrice); + await numberInput.confirmValue(); + + // 选择入库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '采购入库' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认入库' }).click(); + + // 根据备注找出入库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowB = i; + break; + } + } + + // 获取单号 + const billNo = CleanPunctuation( + await page + .locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: remark }), + }) + .locator('td') + .nth(1) + .innerText(), + ); + expect(billNo).not.toBeNull(); + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + }); + + await test.step('校验入库后数据', async () => { + await page.reload(); + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '统计' }).click(); + // 点击统计下拉框选择入库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '入库统计' }).click(); + // 选择产品 + await page.locator('.keyword_search input').fill(productNum); + + await expect(async () => { + // 点击搜索 + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + await page + .locator('.main-table-body_tr') + .first() + .locator('td', { hasText: productNum }) + .waitFor({ timeout: 2000 }); + }).toPass({ timeout: 60000 }); + + // 获取入库后总数 + await expect(page.locator('.main-table-body_tr td').nth(5)).toContainText( + `${Number(totality) + Number(quantity1)}`, + ); + // 获取入库后总额 + await expect(page.locator('.main-table-body_tr td').nth(6)).toContainText( + `${Number(GrossAmount) + Number(quantity1) * Number(UnitPrice)}`, + ); + + // 点击总数进入流水 + await page.locator('.main-table-body_tr td').nth(5).click(); + + // 查看期初数量 + const InitialQuantity = Number( + KeepOnlyNumbers( + await page + .locator('.hideTop .table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td') + .nth(4) + .innerText(), + ), + ); + + // 查看库存变动 + const alteration = Number( + KeepOnlyNumbers( + await page + .locator('.hideTop .table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td') + .nth(5) + .innerText(), + ), + ); + + // 查看库存变动 + const surplus = Number( + KeepOnlyNumbers( + await page + .locator('.hideTop .table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td') + .nth(6) + .innerText(), + ), + ); + + // 结余 = 期初 + 库存变动 + expect(surplus).toBe(InitialQuantity + alteration); + }); + + await test.step('反审清理数据', async () => { + await page.reload(); + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.locator('.outInStock_filter .search_select').last().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '入库单' }).click(); + + // 根据备注找入库单在第几行 + let nowRowC = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowC = i; + break; + } + } + + // 反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowC) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + }); + }); + test('查询', async ({ page, homeNavigation }) => { + const remarkA = faker.helpers.fromRegExp(/1[0-9]{10}/); + const remarkB = faker.helpers.fromRegExp(/1[0-9]{10}/); + // 定义一个卖品和非卖品 + const productNameA = ProjectName.Product.Product_11.name; + const productNoA = ProjectName.Product.Product_11.num; + const productNameB = ProjectName.Product.Product_12.name; + const productNoB = ProjectName.Product.Product_12.num; + + await test.step('卖品入库一单', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^入\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 点击卖品-产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNoA }), + }) + .filter({ + has: page.locator('td', { hasText: productNameA }), + }) + .locator('td') + .nth(0) + .click(); + + // 选择入库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '采购入库' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remarkA); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认入库' }).click(); + + // 根据备注找出入库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remarkA)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + }); + + await test.step('非卖品入库一单', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^入\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 点击非卖品-产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNoB }), + }) + .filter({ + has: page.locator('td', { hasText: productNameB }), + }) + .locator('td') + .nth(0) + .click(); + + // 选择入库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '采购入库' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remarkB); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认入库' }).click(); + + // 根据备注找出入库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remarkB)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + }); + + await test.step('校验', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '统计' }).click(); + // 点击统计下拉框选择入库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '入库统计' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 不勾选非卖品 + await expect(async () => { + await page.locator('.needsclick', { hasText: /^非卖品$/ }).click(); + await expect( + page.locator('.ant-checkbox-wrapper-checked', { hasText: /^非卖品$/ }), + ).not.toBeVisible({ timeout: 2000 }); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 判断刚入库的卖品存在 非卖品不存在 + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoA })).toBeVisible({ + timeout: 2000, + }); + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoB })).not.toBeVisible({ + timeout: 2000, + }); + }).toPass(); + + // 不勾选非卖品 + await expect(async () => { + await page.locator('.needsclick', { hasText: /^非卖品$/ }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: /^非卖品$/ })).toBeVisible({ + timeout: 2000, + }); + await page.locator('.needsclick', { hasText: /^卖品$/ }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: /^卖品$/ })).not.toBeVisible({ + timeout: 2000, + }); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 判断刚入库的卖品不存在 卖品不存在 + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoA })).not.toBeVisible({ + timeout: 2000, + }); + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoB })).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + }); + + await test.step('反审清理数据', async () => { + await page.reload(); + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.locator('.outInStock_filter .search_select').last().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '入库单' }).click(); + + // 根据备注找入库单A在第几行 + let nowRowA = 0; + const allTrA = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countA = await allTrA.count(); + for (let i = 0; i < countA; i++) { + const trA = allTrA.nth(i); + const productA = await trA.locator('td').nth(-2).innerText(); + if (productA.includes(remarkA)) { + nowRowA = i; + break; + } + } + + // 根据备注找入库单B在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remarkB)) { + nowRowB = i; + break; + } + } + + // 反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowA) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + // 反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + }); + }); + + test.describe('出库统计', () => { + test('出库流水', async ({ page, homeNavigation, numberInput }) => { + const remark = faker.helpers.fromRegExp(/1[0-9]{10}/); + const productA = ProjectName.Product.Product_5.name; + const productNum = ProjectName.Product.Product_5.num; + + const quantity1 = 20; //数量 + const UnitPrice = 10; //单价 + /**@type {string} 单号*/ + let billNo; + let totality = 0; + + await test.step('获取出库前总数', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).waitFor(); + await page.locator('.tab_item', { hasText: '统计' }).click(); + // 点击统计下拉框选择出库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '出库统计' }).click(); + // 选择门店 + await page.locator('.shop_picker').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor(); + await page.locator('.compicker_part-box .label', { hasText: 'AT测试一店' }).click(); + await page.locator('.compicker_part-box .label', { hasText: '总部' }).click(); + await page.getByRole('button', { name: '确定选择' }).click(); + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + await page.locator('.keyword_search input').fill(productNum); + // 点击搜索 + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + await page.locator('.ant-input-clear-icon').click(); + await page.locator('.keyword_search input').fill(productNum); + await expect(async () => { + // 点击搜索 + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + const NotFound = page.locator('.m-table__empty', { hasText: '抱歉,未搜索到相关数据' }); + if (await NotFound.isVisible()) { + totality = 0; + } else { + await page + .locator('.main-table-body_tr') + .first() + .locator('td', { hasText: productNum }) + .waitFor({ timeout: 2000 }); + + // 获取总数 + totality = KeepOnlyNumbers(await page.locator('.main-table-body_tr td').nth(5).innerText()); + console.log('前' + totality); + } + }).toPass({ timeout: 60000 }); + }); + + await test.step('出库', async () => { + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^出\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 点击该产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNum }), + }) + .filter({ + has: page.locator('td', { hasText: productA }), + }) + .locator('td') + .nth(0) + .click(); + // 点击设置 + await page.locator('.set_icon').click(); + // 等待弹窗出来 + await page.locator('.popup_content .content').waitFor(); + // 点击显示单价和小计 + await page.locator('.set_btn .ant-checkbox-input ').check(); + // 确认 + await page.locator('.ant-btn-lg', { hasText: /^确\s认$/ }).click(); + // 选择库存数量 + await page.locator('.m-table-cell_warp').first().click(); + await numberInput.setValue(quantity1); + await numberInput.confirmValue(); + + // 点击修改单价 + await page.locator('.m-table-cell_warp').last().click(); + await numberInput.setValue(UnitPrice); + await numberInput.confirmValue(); + + // 选择出库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '产品销售' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认出库' }).click(); + + // 根据备注找出 出库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowB = i; + break; + } + } + + // 获取单号 + const billNo = CleanPunctuation( + await page + .locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: remark }), + }) + .locator('td') + .nth(1) + .innerText(), + ); + console.log('单号' + billNo); + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + }); + + await test.step('校验出库后数据', async () => { + await page.locator('.tab_item', { hasText: '统计' }).click(); + // 点击统计下拉框选择出库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '出库统计' }).click(); + // 页面变化但是内容却是上一页的 硬等2秒加载 + await page.waitForTimeout(2000); + // 选择产品 + await page.locator('.keyword_search input').fill(productNum); + await expect(async () => { + // 点击搜索 + await page.locator('.keyword_search .ant-input-suffix .search_icon').click(); + await page + .locator('.main-table-body_tr') + .first() + .locator('td', { hasText: productNum }) + .waitFor({ timeout: 2000 }); + }).toPass({ timeout: 60000 }); + + // 获取出库后总数 + await expect(page.locator('.main-table-body_tr td').nth(5)).toBe(Number(totality) + Number(quantity1)); + + // 点击总数进入流水 + await page.locator('.main-table-body_tr td').nth(5).click(); + + // 查看期初数量 + const InitialQuantity = Number( + KeepOnlyNumbers( + await page + .locator('.hideTop .table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td') + .nth(4) + .innerText(), + ), + ); + + // 查看库存变动 + const alteration = Number( + KeepOnlyNumbers( + await page + .locator('.hideTop .table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td') + .nth(5) + .innerText(), + ), + ); + + // 查看结余 + const surplus = Number( + KeepOnlyNumbers( + await page + .locator('.hideTop .table-warp .m-table__body-wrapper .m-table__body tbody tr') + .filter({ + has: page.locator('td', { hasText: billNo }), + }) + .locator('td') + .nth(6) + .innerText(), + ), + ); + + // 结余 = 期初 + 库存变动(这里拿到正数 所以是减) + expect(surplus).toBe(InitialQuantity - alteration); + }); + + await test.step('反审清理数据', async () => { + await page.reload(); + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.locator('.outInStock_filter .search_select').last().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '出库单' }).click(); + + // 根据备注找出库单在第几行 + let nowRowC = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remark)) { + nowRowC = i; + break; + } + } + + // 反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowC) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + }); + }); + test('查询', async ({ page, homeNavigation }) => { + const remarkA = faker.helpers.fromRegExp(/1[0-9]{10}/); + const remarkB = faker.helpers.fromRegExp(/1[0-9]{10}/); + // 定义一个卖品和非卖品 + const productNameA = ProjectName.Product.Product_11.name; + const productNoA = ProjectName.Product.Product_11.num; + const productNameB = ProjectName.Product.Product_12.name; + const productNoB = ProjectName.Product.Product_12.num; + + await test.step('卖品出库一单', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^出\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 点击卖品-产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNoA }), + }) + .filter({ + has: page.locator('td', { hasText: productNameA }), + }) + .locator('td') + .nth(0) + .click(); + + // 选择出库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '产品销售' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remarkA); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认出库' }).click(); + + // 根据备注找出 出库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remarkA)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + }); + + await test.step('非卖品出库一单', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.getByRole('button', { name: /^出\s库$/ }).click(); + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 点击非卖品-产品 + await page + .locator('.panel_report tbody tr') + .filter({ + has: page.locator('td', { hasText: productNoB }), + }) + .filter({ + has: page.locator('td', { hasText: productNameB }), + }) + .locator('td') + .nth(0) + .click(); + + // 选择出库方式 + await page.locator('.bill_form .ant-select-selection__rendered').click(); + await page.getByRole('option', { name: '产品销售' }).click(); + + // 点击备注 + await page.locator('.bill_remark > .icon > svg').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remarkB); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page.getByRole('button', { name: '确认出库' }).click(); + + // 根据备注找出 出库单在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remarkB)) { + nowRowB = i; + break; + } + } + + // 点击明细 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '明细' }) + .click(); + await page.getByRole('button', { name: /^审\s核$/ }).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 等待弹窗消失 + await expect(page.locator('.popup_content')).not.toBeVisible(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + }); + + await test.step('校验', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '统计' }).click(); + // 点击统计下拉框选择出库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '出库统计' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 不勾选非卖品 + await expect(async () => { + await page.locator('.needsclick', { hasText: /^非卖品$/ }).click(); + await expect( + page.locator('.ant-checkbox-wrapper-checked', { hasText: /^非卖品$/ }), + ).not.toBeVisible({ timeout: 2000 }); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 判断刚入库的卖品存在 非卖品不存在 + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoA })).toBeVisible({ + timeout: 2000, + }); + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoB })).not.toBeVisible({ + timeout: 2000, + }); + }).toPass(); + + // 不勾选非卖品 + await expect(async () => { + await page.locator('.needsclick', { hasText: /^非卖品$/ }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: /^非卖品$/ })).toBeVisible({ + timeout: 2000, + }); + await page.locator('.needsclick', { hasText: /^卖品$/ }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: /^卖品$/ })).not.toBeVisible({ + timeout: 2000, + }); + // 等待加载完成 + await expect(page.locator('.m-table__loading')).toBeHidden(); + // 判断刚入库的卖品不存在 卖品不存在 + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoA })).not.toBeVisible({ + timeout: 2000, + }); + await expect(page.locator('.m-table__body-wrapper td', { hasText: productNoB })).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + }); + + await test.step('反审清理数据', async () => { + await page.reload(); + // 点击库存 + await homeNavigation.gotoModule('库存'); + // 点击出入库管理 + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + await page.locator('.outInStock_filter .search_select').last().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '出库单' }).click(); + + // 根据备注找出库单A在第几行 + let nowRowA = 0; + const allTrA = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countA = await allTrA.count(); + for (let i = 0; i < countA; i++) { + const trA = allTrA.nth(i); + const productA = await trA.locator('td').nth(-2).innerText(); + if (productA.includes(remarkA)) { + nowRowA = i; + break; + } + } + + // 根据备注找出库单B在第几行 + let nowRowB = 0; + const allTrB = page.locator('.table-warp .m-table__body-wrapper .m-table__body tbody tr'); + const countB = await allTrB.count(); + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const productB = await trB.locator('td').nth(-2).innerText(); + if (productB.includes(remarkB)) { + nowRowB = i; + break; + } + } + + // 反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowA) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + // 反审 + await page + .locator('.m-table__fixed-right .m-table__body tbody tr') + .nth(nowRowB) + .locator('td') + .nth(-1) + .locator('.m-table-cell_btn', { hasText: '反审' }) + .click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + }); + }); + + test.describe('产品进销存表', () => { + test('查询', async ({ page, homeNavigation, tablePage, customerPage, createCustomer, numberInput }) => { + const productA = ProjectName.Product.Product_13.name; + const productNum = ProjectName.Product.Product_13.num; + // 创建顾客 + const customer = createCustomer; + + // 定义转入数量 + let Buy = '5'; + let InQuantity = '2'; //转寄存 + let expend = '1'; //领出 + // 汇总前数据 + let FrontInQuantity; //入库 + let FrontOutQuantity; //出库 + // 明细前数据 + let FrontInDeposit; //转入 + let FrontOutDeposit; //领出 + + await test.step('记录汇总初始数据', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).waitFor(); + await page.locator('.tab_item', { hasText: '统计' }).click(); + + // 点击统计下拉框选择出库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '产品进销存' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 点击门店选择器 + await page.locator('.shop_picker').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor(); + // 选择一店 + await expect(async () => { + await page.locator('.compicker_part-box .label', { hasText: 'AT测试一店' }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + + // 取消总部 + await expect(async () => { + await page.locator('.compicker_part-box .label', { hasText: '总部' }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: '总部' })).not.toBeVisible({ + timeout: 2000, + }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 输入A产品编号查询 + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(productNum); + await expect(async () => { + // 点击搜索 + await page + .locator('.ant-input-affix-wrapper-input-with-clear-btn .ant-input-suffix .search_icon') + .click(); + await expect( + page + .locator('.m-table__body-wrapper .main-table-body_tr .auto_desc', { hasText: productA }) + .first(), + ).toBeVisible({ timeout: 2000 }); + }).toPass(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 入库排序索引 + const inToInventory = await tablePage.getFirstHeaderTableIndex('入库'); + // 根据条件定位 + const goodsIndex = await tablePage.getBodyTableIndex(['男士精油', 'aa100015', 'oil']); + FrontInQuantity = await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(inToInventory + 2) + .innerText(); + + // 出库排序索引 + const outInventory = await tablePage.getFirstHeaderTableIndex('出库'); + // 根据条件定位 + FrontOutQuantity = await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(outInventory + 3) + .innerText(); + console.log('出:' + FrontOutQuantity); + }); + + await test.step('记录明细数据', async () => { + // 点击明细 + await page.locator('.ant-switch').first().click(); + await expect(page.locator('.ant-switch-checked')).toBeVisible(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + await expect(async () => { + // 点击搜索强制刷新下面的数据(可能不出来) + await page + .locator('.ant-input-affix-wrapper-input-with-clear-btn .ant-input-suffix .search_icon') + .click(); + await expect(page.getByText('产品销售').first()).toBeVisible({ timeout: 2000 }); + }).toPass(); + + // 寄存转入排序索引 + const inToInventory = await tablePage.getFirstHeaderTableIndex('寄存转入'); + // 根据条件定位 + const goodsIndex = await tablePage.getBodyTableIndex(['男士精油', 'aa100015', 'oil']); + await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(inToInventory + 12) + .click(); + FrontInDeposit = await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(inToInventory + 12) + .innerText(); + console.log('转入:' + FrontInDeposit); + + // 寄存领出排序索引 + const outInventory = await tablePage.getFirstHeaderTableIndex('寄存领出'); + // 根据条件定位 + await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(outInventory + 13) + .click(); + FrontOutDeposit = await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(outInventory + 13) + .innerText(); + console.log('领出:' + FrontOutDeposit); + }); + + await test.step('顾客购买寄存', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + + await page.getByText('卖品', { exact: true }).click(); + + await page.locator('.project_list .number').filter({ hasText: productNum }).click(); + + // 点击选择数量 + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(Number(Buy)); + await numberInput.confirmValue(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + // 寄存 + await page.getByRole('button', { name: '转寄存' }).click(); + await page + .locator('.popup_content') + .filter({ has: page.locator('.title', { hasText: '产品寄存' }) }) + .locator('.num') + .click(); + await numberInput.setValue(Number(InQuantity)); + await numberInput.confirmValue(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '寄存成功' })).toBeVisible(); + }); + + await test.step('领出一次寄存品', async () => { + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + + // 收银-点击领出寄存品 + await page.locator('.treat_card_inner').click(); + await page.locator('.name_text', { hasText: productA }).waitFor(); + + // 收银结算 + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + // 支付结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + await test.step('汇总核对数据', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).waitFor(); + await page.locator('.tab_item', { hasText: '统计' }).click(); + + // 点击统计下拉框选择出库统计 + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '产品进销存' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 点击门店选择器 + await page.locator('.shop_picker').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor(); + // 选择一店 + await expect(async () => { + await page.locator('.compicker_part-box .label', { hasText: 'AT测试一店' }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: 'AT测试一店' })).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + + // 取消总部 + await expect(async () => { + await page.locator('.compicker_part-box .label', { hasText: '总部' }).click(); + await expect(page.locator('.ant-checkbox-wrapper-checked', { hasText: '总部' })).not.toBeVisible({ + timeout: 2000, + }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 输入A产品编号查询 + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(productNum); + await expect(async () => { + // 点击搜索 + await page + .locator('.ant-input-affix-wrapper-input-with-clear-btn .ant-input-suffix .search_icon') + .click(); + await expect( + page + .locator('.m-table__body-wrapper .main-table-body_tr .auto_desc', { hasText: productA }) + .first(), + ).toBeVisible({ timeout: 2000 }); + }).toPass(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + + // 入库排序索引 + const inToInventory = await tablePage.getFirstHeaderTableIndex('入库'); + // 根据条件定位 + const goodsIndex = await tablePage.getBodyTableIndex(['男士精油', 'aa100015', 'oil']); + // 校验 + await expect + .soft( + tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(inToInventory + 2), + ) + .toContainText(`${Number(FrontInQuantity) + Number(InQuantity)}`); + + // 出库排序索引 + const outInventory = await tablePage.getFirstHeaderTableIndex('出库'); + // 校验 + await expect( + tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(outInventory + 3), + ).toContainText(`${Number(FrontOutQuantity) + Number(Buy) + Number(expend)}`); + }); + + await test.step('明细核对数据', async () => { + // 点击明细 + await page.locator('.ant-switch').first().click(); + await expect(page.locator('.ant-switch-checked')).toBeVisible(); + // 等待加载完成 + await expect(page.locator('.m-table__loading').first()).toBeHidden(); + await expect(async () => { + // 点击搜索强制刷新下面的数据(可能不出来) + await page + .locator('.ant-input-affix-wrapper-input-with-clear-btn .ant-input-suffix .search_icon') + .click(); + await expect(page.getByText('产品销售').first()).toBeVisible({ timeout: 2000 }); + }).toPass(); + + // 寄存转入排序索引 + const inToInventory = await tablePage.getFirstHeaderTableIndex('寄存转入'); + // 根据条件定位 + const goodsIndex = await tablePage.getBodyTableIndex(['男士精油', 'aa100015', 'oil']); + await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(inToInventory + 12) + .click(); + + // 校验 + await expect( + tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(inToInventory + 12), + ).toContainText(`${Number(FrontInDeposit) + Number(InQuantity)}`); + + // 寄存领出排序索引 + const outInventory = await tablePage.getFirstHeaderTableIndex('寄存领出'); + // 根据条件定位 + await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(outInventory + 13) + .click(); + // 校验 + await expect( + tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(outInventory + 13), + ).toContainText(`${Number(FrontOutDeposit) + Number(expend)}`); + }); + }); + }); + + test.describe('开用进销存表', async () => { + test('查询', async ({ page, homeNavigation, createCustomer, tablePage, customerPage, numberInput }) => { + const customer = createCustomer; + + // 产品 + const goods = { + no: 'aa100627', + name: '平衡净肤面膜', + capacity: 5, + unit: '毫升', + price: 10, + }; + + let consume; + await test.step('查询开用进销存表数据', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '统计' }).click(); + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '开用进销存' }).click(); + await page.getByText('汇总').waitFor(); + + // 搜索产品 + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goods.no); + await expect(async () => { + await page.locator('.search_icon > svg').first().click(); + await expect( + tablePage.bodyTrTable + .filter({ + hasText: goods.name, + }) + .filter({ + hasText: goods.no, + }) + .first(), + ).toBeVisible({ timeout: 2000 }); + }).toPass(); + + // 消耗排序索引 + const consumeIndex = await tablePage.getFirstHeaderTableIndex('消耗'); + + // 产品索引 + const goodsIndex = await tablePage.getBodyTableIndex([goods.name, goods.no, goods.unit]); + + // 获取消耗值 + consume = await tablePage.bodyTrTable + .nth(goodsIndex) + .locator('td') + .nth(consumeIndex) + .innerText() + .then(Number); + }); + + /**@type {string} 单号*/ + let billNo; + await test.step('顾客开单购买3次项目,并消耗3次项目,选择产品配方3次5ml', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.phone); + + // 购买项目3次 + await page + .locator('.project_list .number') + .nth(faker.number.int({ min: 0, max: 20 })) + .click(); + await page.locator('#buyList').getByText('1', { exact: true }).click(); + await numberInput.setValue(3); + await numberInput.confirmValue(); + + // 打开项目配方设置 + await page.locator('.staff_setting').click(); + await page.getByLabel('项目配方').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗项目3次 + await page.locator('.commodity_item').first().click(); + await page.getByRole('main').getByText('1', { exact: true }).click(); + await numberInput.setValue(3); + await numberInput.confirmValue(); + + // 选择产品配方 + await page.locator('.formula_noData').click(); + await page.getByRole('textbox').fill(goods.no); + await page.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await page.getByLabel('平衡净肤面膜').uncheck(); + await page.getByLabel('平衡净肤面膜').check(); + await expect(page.locator('.menu-item-dot', { hasText: '1' }).first()).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + // 单次消耗5ml + await page.getByRole('textbox').fill('5'); + await page.getByRole('button', { name: /保\s存/ }).click(); + + // 使用现金支付 + await page.getByText(/结\s算/).click(); + await page.locator('.pay_img').first().click(); + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /结\s算/ }).click(), + ]); + billNo = (await response.json())?.content?.billNo; + expect(billNo).not.toBeNull(); + await expect(page.getByRole('button', { name: /开\s单/ })).toBeVisible(); + }); + + await test.step('查询开用进销存表数据', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '统计' }).click(); + await page.locator('.router_active .ant-dropdown-link').click(); + await page.getByRole('menuitem', { name: '开用进销存' }).click(); + await page.getByText('汇总').waitFor(); + + // 搜索产品 + await page.getByPlaceholder('编码/名称/条码/拼音码').fill(goods.no); + await expect(async () => { + await page.locator('.search_icon > svg').first().click(); + await expect( + tablePage.bodyTrTable + .filter({ + hasText: goods.name, + }) + .filter({ + hasText: goods.no, + }) + .first(), + ).toBeVisible({ timeout: 2000 }); + }).toPass(); + + // 消耗排序索引 + const consumeIndex = await tablePage.getFirstHeaderTableIndex('消耗'); + + // 产品索引 + const goodsIndex = await tablePage.getBodyTableIndex([goods.name, goods.no, goods.unit]); + + // 获取消耗值 + const $lastConsume = tablePage.bodyTrTable.nth(goodsIndex).locator('td').nth(consumeIndex); + + // 消耗值应该增加 + await expect($lastConsume).toContainText(`${consume + 15}`); + + await tablePage.bodyTrTable.nth(goodsIndex).locator('td').nth(consumeIndex).click(); + const $popup = page.locator('.popup_content'); + const $popupTr = $popup + .locator('.m-table__body-wrapper .m-table__body tbody tr') + .filter({ hasText: billNo }); + + await expect($popupTr).toBeVisible(); + + await expect($popupTr.locator('td').nth(5)).toHaveText('3'); + await expect($popupTr.locator('td').nth(6)).toHaveText('-15'); + }); + }); + }); +}); + +test.describe('调货管理', () => { + test.beforeEach(async ({ page }) => { + await page.addLocatorHandler(page.getByRole('button', { name: '暂不处理' }), async () => { + await page.getByRole('button', { name: '暂不处理' }).click(); + await expect(page.getByRole('button', { name: '暂不处理' })).not.toBeVisible(); + }); + }); + + test('调货管理', async ({ page, homeNavigation, transferManagementPage }) => { + const goods = { + a: { no: 'aa100007', name: '肌因能量套', unitPrice: 10, quantity: 5 }, + b: { no: 'aa100008', name: '肌因赋活尊享套', unitPrice: 15, quantity: 8 }, + }; + const remarkA = '调货管理' + faker.helpers.fromRegExp(/[a-d]{2}[0-9]{2}/); + const remarkB = '调货管理' + faker.helpers.fromRegExp(/[a-d]{2}[0-9]{2}/); + let billA, billB; + + let quantity = 0; // 数量 + let outQuantity = 0; // 调出数量 + let unitPrice = 0; // 单价 + let outUnitPrice = 0; // 调出单价 + let subtotal = 0; // 小计 + let outSubtotal = 0; // 调出小计 + let totalQuantity = 0; // 总数量 + let totalPrice = 0; // 总价 + + await test.step('进入门店要货', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.top_tab .tab_item').getByText('调货管理').click(); + await transferManagementPage.gotoSubPage('门店要货'); + }); + + await test.step('提交要货单A', async () => { + // 展示单价和小计 + await page.locator('.price_set').click(); + await page.locator('.set_btn').getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 选择产品a,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.a.no }).click(); + let goodsTr = page.locator('.bill_report tr', { hasText: goods.a.name }); + await goodsTr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.quantity)); + await page.locator('.number_tr button').nth(11).click(); + await goodsTr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.unitPrice)); + await page.locator('.number_tr button').nth(11).click(); + + // 选择产品b,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.b.no }).click(); + goodsTr = page.locator('.bill_report tr', { hasText: goods.b.name }); + await goodsTr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.quantity)); + await page.locator('.number_tr button').nth(11).click(); + await goodsTr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.unitPrice)); + await page.locator('.number_tr button').nth(11).click(); + + // 设置备注 + await page.getByText('备注').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remarkA); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 提交要货单 + await page.getByRole('button', { name: /提\s交/ }).click(); + + await expect(page.getByRole('cell', { name: '门店要货备注' })).toBeInViewport(); + + const tableTrList = page.locator('.table_inner .main-table-body_tr'); + const billTr = tableTrList.filter({ hasText: remarkA }); + await expect(billTr).toBeVisible(); + billA = await billTr.locator('td').nth(0).innerText(); + expect(billA).not.toBeNull(); + }); + + await test.step('提交要货单B', async () => { + await transferManagementPage.gotoSubPage('门店要货'); + // 展示单价和小计 + await page.locator('.price_set').click(); + await page.locator('.set_btn').getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 选择产品a,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.a.no }).click(); + let goodsTr = page.locator('.bill_report tr', { hasText: goods.a.name }); + await goodsTr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.quantity)); + await page.locator('.number_tr button').nth(11).click(); + await goodsTr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.unitPrice)); + await page.locator('.number_tr button').nth(11).click(); + + // 选择产品b,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.b.no }).click(); + goodsTr = page.locator('.bill_report tr', { hasText: goods.b.name }); + await goodsTr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.quantity)); + await page.locator('.number_tr button').nth(11).click(); + await goodsTr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.unitPrice)); + await page.locator('.number_tr button').nth(11).click(); + + // 设置备注 + await page.getByText('备注').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remarkB); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 提交要货单 + await page.getByRole('button', { name: /提\s交/ }).click(); + + await expect(page.getByRole('cell', { name: '门店要货备注' })).toBeInViewport(); + + const tableTrList = page.locator('.table_inner .main-table-body_tr'); + const billTr = tableTrList.filter({ hasText: remarkB }); + await expect(billTr).toBeVisible(); + billB = await billTr.locator('td').nth(0).innerText(); + expect(billB).not.toBeNull(); + }); + + await test.step('标记要货单A为处理中-》生成内部调出单', async () => { + // 进入调货管理 + await transferManagementPage.gotoSubPage('调货管理'); + // 标记为处理中 + const tableTrList = page.locator('.table_inner .main-table-body_tr'); + const targetTr = tableTrList.filter({ hasText: billA }); + const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList); + const fixedCell = page.locator('.m-table-fixed-body tr').nth(targetIndex); + await fixedCell.getByText('标记为处理中').click(); + await page.getByRole('button', { name: '我知道了' }).click(); + + await fixedCell.getByText('生成内部调出单').click(); + + await expect(page.locator('.popup_content', { hasText: '要货单-生成内部调出单' })).toBeVisible(); + + const outBillGoodsList = page.locator('.popup_content .main-table-body_tr'); + // 设置产品a的调出单价和数量 + let goodsTr = outBillGoodsList.filter({ hasText: goods.a.no }); + await goodsTr + .locator('td') + .nth(7) + .locator('input') + .fill(`${goods.a.unitPrice + 1}`); + await goodsTr + .locator('td') + .nth(8) + .locator('input') + .fill(`${goods.a.quantity + 1}`); + + // 设置产品b的调出单价和数量 + goodsTr = outBillGoodsList.filter({ hasText: goods.b.no }); + await goodsTr + .locator('td') + .nth(7) + .locator('input') + .fill(`${goods.b.unitPrice + 1}`); + await goodsTr + .locator('td') + .nth(8) + .locator('input') + .fill(`${goods.b.quantity + 1}`); + + // 生产内部调出单 + await page.getByRole('button', { name: /保\s存/ }).click(); + await page.getByRole('button', { name: '我知道了' }).click(); + }); + + await test.step('查看内部调货单A', async () => { + // 查看内部调货单A + const tableTrList = page.locator('.table_inner .main-table-body_tr'); + const targetTr = tableTrList.filter({ hasText: billA }); + const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList); + const fixedCell = page.locator('.m-table-fixed-body tr').nth(targetIndex); + + await fixedCell.getByText('调货单').click(); + + // 拿取商品a的数据 + let goodsTr = page.locator('.popup_content tr', { hasText: goods.a.name }); + quantity = Number(await goodsTr.locator('td').nth(5).innerText()); + outQuantity = Number(await goodsTr.locator('td').nth(6).innerText()); + unitPrice = Number(await goodsTr.locator('td').nth(7).innerText()); + outUnitPrice = Number(await goodsTr.locator('td').nth(8).innerText()); + subtotal = Number(await goodsTr.locator('td').nth(9).innerText()); + outSubtotal = Number(await goodsTr.locator('td').nth(10).innerText()); + totalPrice += quantity * unitPrice; + totalQuantity += quantity; + + // 判断商品a的数据 + expect.soft(quantity).toBe(goods.a.quantity); + expect.soft(unitPrice).toBe(goods.a.unitPrice); + expect.soft(outQuantity).toBe(goods.a.quantity + 1); + expect.soft(outUnitPrice).toBe(goods.a.unitPrice + 1); + expect.soft(subtotal).toBe(goods.a.quantity * goods.a.unitPrice); + expect(outSubtotal).toBe((goods.a.quantity + 1) * (goods.a.unitPrice + 1)); + + // 拿取商品b的数据 + goodsTr = page.locator('.popup_content tr', { hasText: goods.b.name }); + quantity = Number(await goodsTr.locator('td').nth(5).innerText()); + outQuantity = Number(await goodsTr.locator('td').nth(6).innerText()); + unitPrice = Number(await goodsTr.locator('td').nth(7).innerText()); + outUnitPrice = Number(await goodsTr.locator('td').nth(8).innerText()); + subtotal = Number(await goodsTr.locator('td').nth(9).innerText()); + outSubtotal = Number(await goodsTr.locator('td').nth(10).innerText()); + totalPrice += quantity * unitPrice; + totalQuantity += quantity; + + // 判断商品b的数据 + expect.soft(quantity).toBe(goods.b.quantity); + expect.soft(unitPrice).toBe(goods.b.unitPrice); + expect.soft(outQuantity).toBe(goods.b.quantity + 1); + expect.soft(outUnitPrice).toBe(goods.b.unitPrice + 1); + expect.soft(subtotal).toBe(goods.b.quantity * goods.b.unitPrice); + expect.soft(outSubtotal).toBe((goods.b.quantity + 1) * (goods.b.unitPrice + 1)); + + // 判断合计数量、合计金额 + expect.soft(totalPrice).toBe(goods.b.quantity * goods.b.unitPrice + goods.a.quantity * goods.a.unitPrice); + expect(totalQuantity).toBe(goods.a.quantity + goods.b.quantity); + + // 关闭弹窗 + await page.locator('.title > .close_icon > svg').click(); + }); + + await test.step('驳回要货单B', async () => { + // 查看内部调货单B + const tableTrList = page.locator('.table_inner .main-table-body_tr'); + const targetTr = tableTrList.filter({ hasText: billB }); + const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList); + const fixedCell = page.locator('.m-table-fixed-body tr').nth(targetIndex); + await fixedCell.getByText('驳回').click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await transferManagementPage.gotoSubPage('要货单'); + + const billBTr = page + .locator('.m-table__body-wrapper .main-table-body_tr') + .filter({ hasText: billB }) + .locator('td', { hasText: '被驳回' }); + await expect(billBTr).toBeVisible(); + }); + }); +}); + +test.describe('调货管理', () => { + // 此处不需要beforeEach将消息弹窗去除 + test('门店要货提醒', async ({ page, homeNavigation, transferManagementPage, tablePage }) => { + const goods = { no: 'aa100007', name: '肌因能量套', unitPrice: 10, quantity: 5 }; + + const remark = '门店要货提醒' + faker.helpers.fromRegExp(/[a-d]{2}[0-9]{2}/); + + await test.step('进入门店要货', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.top_tab .tab_item').getByText('调货管理').click(); + await transferManagementPage.gotoSubPage('门店要货'); + }); + + const $$tableTrList = page.locator('.table_inner .main-table-body_tr'); + const $billTr = $$tableTrList.filter({ hasText: remark }); + + await test.step('保存要货单,查看要货单提醒', async () => { + // 选择产品a,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.no }).click(); + + // 设置备注 + await page.getByText('备注').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 保存要货单 + await page.getByRole('button', { name: /保\s存/ }).click(); + + await expect(page.getByRole('cell', { name: '门店要货备注' })).toBeInViewport(); + await expect($billTr).toBeVisible(); + await expect(page.getByText('门店要货', { exact: true })).not.toBeVisible(); + await expect(page.getByRole('button', { name: '去处理' })).not.toBeVisible(); + }); + + await test.step('提交要货单,查看要货单提醒', async () => { + const billIndex = await tablePage.getBodyTableIndex([remark]); + + const $$rightTr = tablePage.fixedRightTable.locator('tbody tr'); + + await $$rightTr.nth(billIndex).getByText('编辑').click(); + + // 提交要货单 + await page.getByRole('button', { name: /提\s交/ }).click(); + + // await expect(page.getByText('门店要货', { exact: true })).toBeVisible(); + const storeName = 'AT测试一店'; + await expect(page.getByText(`慧来客提醒您,${storeName}申请调货了~`)).toBeVisible(); + await expect(page.getByRole('button', { name: '去处理' })).toBeVisible(); + }); + }); +}); + +test.describe('过期预警表', () => { + test('查询', async ({ page, homeNavigation, customerPage, createCustomer, numberInput }) => { + const product = ProjectName.Product.Product_11.name; + const productNum = ProjectName.Product.Product_11.num; + + let code; // 过期预警表代码段 + let lastSurplus; // 初始数量 + let lastOffSurplus; // 初始关闭数量 + const customer = createCustomer; + + await test.step('记录初始剩余数量', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '库存管理' }).click(); + await page.locator('.tab_option', { hasText: '过期预警表' }).click(); + await page.locator('.toolbar', { hasText: '寄存' }).waitFor(); + await expect(async () => { + // 点击门店选择器 + await page.locator('.shop_picker').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor({ timeout: 2000 }); + }).toPass(); + // 选择一店 + await page.locator('.com_picker').last().locator('.label_item', { hasText: 'AT测试一店' }).click(); + // await expect(page.locator('.com_picker').last().locator('.ant-checkbox-wrapper-checked',{hasText:'AT测试一店'})).toBeVisible({timeout:2000}); + await expect(page.locator('.m-table__loading')).toBeHidden(); + + code = page.locator('.m-table__body-wrapper .m-table__body tr'); + // 获取指定产品初始剩余数量 + lastSurplus = await code + .filter({ has: page.locator('td', { hasText: productNum }) }) + .filter({ has: page.locator('td', { hasText: product }) }) + .locator('td') + .nth(6) + .innerText(); + + // 关闭寄存按钮 + await expect(async () => { + await page.locator('.ant-switch').click(); + await expect(page.locator('.ant-switch-checked')).not.toBeVisible(); + }).toPass(); + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 关闭寄存后产品数量 + lastOffSurplus = await code + .filter({ has: page.locator('td', { hasText: productNum }) }) + .filter({ has: page.locator('td', { hasText: product }) }) + .locator('td') + .nth(6) + .innerText(); + }); + + await test.step('顾客购买指定产品,选择寄存部分', async () => { + await homeNavigation.gotoModule('顾客'); + + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: customer.username }).last().click(); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + + // 点击卖品 + await page.locator('.float_tab').getByText('卖品').click(); + + // 点击一个卖品 + await page.locator('.number_service > .project_list').filter({ hasText: productNum }).click(); + + // 修改数量 + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(5); + await numberInput.confirmValue(); + + // 结算 + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + + // 选择现金支付 + await page.getByText('现金').click(); + // 取消推送消费提醒 + await page.getByLabel('推送消费提醒').uncheck(); + // 取消结算签字 + await page.getByLabel('结算签字').uncheck(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + + await page.locator('.popup_content .title', { hasText: '产品寄存' }).waitFor(); + + // 寄存卖品 + await page.getByRole('button', { name: '转寄存' }).click(); + await page + .locator('.popup_content') + .filter({ has: page.locator('.title', { hasText: '产品寄存' }) }) + .locator('.num') + .click(); + await numberInput.setValue(3); + await numberInput.confirmValue(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + }); + + await test.step('查看寄存开启关闭状态的数量', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '库存管理' }).click(); + await page.locator('.tab_option', { hasText: '过期预警表' }).click(); + await page.locator('.toolbar', { hasText: '寄存' }).waitFor(); + + // 顾客消费寄存后的产品数量 + const Surplus = await code + .filter({ has: page.locator('td', { hasText: productNum }) }) + .filter({ has: page.locator('td', { hasText: product }) }) + .locator('td') + .nth(6) + .innerText(); + expect.soft(Surplus * 1).toBe(lastSurplus * 1 - 2); + + // 关闭寄存按钮 + await expect(async () => { + await page.locator('.ant-switch').click(); + await expect(page.locator('.ant-switch-checked')).not.toBeVisible(); + }).toPass(); + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 关闭寄存后产品数量 + const offSurplus = await code + .filter({ has: page.locator('td', { hasText: productNum }) }) + .filter({ has: page.locator('td', { hasText: product }) }) + .locator('td') + .nth(6) + .innerText(); + expect.soft(offSurplus * 1).toBe(lastOffSurplus * 1 - 5); + }); + }); + test('修改', async ({ page, homeNavigation }) => { + const remarkRandom = faker.helpers.fromRegExp(/1[0-9]{10}/); + + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '库存管理' }).click(); + await page.locator('.tab_option', { hasText: '过期预警表' }).click(); + await page.locator('.toolbar', { hasText: '寄存' }).waitFor(); + await expect(page.locator('.m-table__loading')).toBeHidden(); + + await expect(async () => { + // 选择门店 + await page.locator('.shop_picker').click(); + await page.locator('.comSelect_title', { hasText: /^选择$/ }).waitFor({ timeout: 2000 }); + }).toPass(); + await page.locator('.compicker_part-box .label', { hasText: 'AT测试一店' }).click(); + + // 点击输入框输入条形码 + await page.getByPlaceholder('编码/名称/条码/拼音码').fill('713112'); + await page.locator('.search_icon').first().click(); + await page.locator('.propertyNo').first().waitFor(); + await expect(page.locator('.m-table__loading')).toBeHidden(); + + // 获取当前失效日期 + + // 修改失效日期 + await page.locator('.invalidTime .on_click_cell_icon').click(); + await page.locator('.popup_content .title', { hasText: '选择有效期' }).waitFor(); + await page.getByPlaceholder('请选择日期').click(); + await page.locator('.ant-calendar-panel').waitFor(); + // 点击今天 + await page.locator('.ant-calendar-footer-btn').click(); + await page.locator('.confirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message').waitFor(); + + // 修改备注 + await page.locator('.remark .on_click_cell_icon').click(); + await page.locator('.popup_content .title', { hasText: '修改备注' }).waitFor(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remarkRandom); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message').waitFor(); + + const Time = await page.locator('.invalidTime').innerText(); + const expireDay = await page.locator('.expireDays').innerText(); + const remark = await page.locator('.remark').first().innerText(); + + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, '0'); + const day = String(currentDate.getDate()).padStart(2, '0'); + const formattedDate = `${year}-${month}-${day}`; + + expect.soft(Time).toBe(formattedDate); + expect.soft(expireDay).toBe('1天后过期'); + expect.soft(remark).toBe(remarkRandom); + + // 修改失效日期 + await page.locator('.invalidTime .on_click_cell_icon').click(); + await page.locator('.popup_content .title', { hasText: '选择有效期' }).waitFor(); + // 点击不限期 + await page.locator('.popup_content .content div').first().click(); + await page.locator('.confirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message').waitFor(); + }); +}); + +test.describe.serial('盘点', () => { + test.beforeEach(async ({ page }) => { + // 处理随机的弹窗 + await page.addLocatorHandler(page.locator('.popup_content', { hasText: '门店要货' }), async () => { + await page.locator('.popup_content .close_icon').click(); + await expect(page.locator('.popup_content .close_icon')).not.toBeVisible(); + }); + }); + + test('产品盘点', async ({ page, homeNavigation, numberInput }) => { + const productA = ProjectName.Product.Product_1.name; //产品A + const productB = ProjectName.Product.Product_2.name; //产品B + + const productNumA = ProjectName.Product.Product_1.num; //产品A + const productNumB = ProjectName.Product.Product_2.num; //产品B + + // 库存总量 + let inventoryA; + let inventoryB; + + // 批次数量 + let BatchQuantityA; + let BatchQuantityB; + + // 产品余量 + let ProductMargin; + // 获取单号的ID + let billId; + /**@type {string} 单号*/ + let billNo; + + await test.step('记录各自批次数量', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).waitFor(); + await page.locator('.tab_item', { hasText: '盘点' }).click(); + await page.locator('.comBill-cot_lt').waitFor(); + + // 获取库存总量列数 + let inventory; + const columns = page.locator('.panel_report thead tr th'); + await columns.last().waitFor(); + const headers = await columns.allInnerTexts(); + inventory = headers.findIndex(headerText => headerText.includes('库存总量')); + if (inventory !== -1) { + console.log(`"库存总量" 列是第 ${inventory + 1} 列`); + } else { + throw new Error('没有找到列'); + } + + // 获取批次数量列数 + let BatchQuantity; + await columns.last().waitFor(); + BatchQuantity = headers.findIndex(headerText => headerText.includes('批次数量')); + if (inventory !== -1) { + console.log(`"批次数量" 列是第 ${BatchQuantity + 1} 列`); + } else { + throw new Error('没有找到列'); + } + + // 获取各项目库存总量 + const ProductLine = page.locator('.panel_report .m-table__body tbody tr'); + inventoryA = await ProductLine.filter({ has: page.locator('td', { hasText: productA }) }) + .filter({ has: page.locator('td', { hasText: productNumA }) }) + .locator('td') + .nth(inventory) + .innerText(); + inventoryB = await ProductLine.filter({ has: page.locator('td', { hasText: productB }) }) + .filter({ has: page.locator('td', { hasText: productNumB }) }) + .locator('td') + .nth(inventory) + .innerText(); + console.log(inventoryA, inventoryB); + + // 获取各项目批次数量 + BatchQuantityA = Number( + await ProductLine.filter({ has: page.locator('td', { hasText: productA }) }) + .filter({ has: page.locator('td', { hasText: productNumA }) }) + .locator('td') + .nth(BatchQuantity) + .innerText(), + ); + BatchQuantityB = Number( + await ProductLine.filter({ has: page.locator('td', { hasText: productB }) }) + .filter({ has: page.locator('td', { hasText: productNumB }) }) + .locator('td') + .nth(BatchQuantity) + .innerText(), + ); + console.log(BatchQuantityA, BatchQuantityB); + }); + + await test.step('输入各产品指定数量并查看', async () => { + // A 输入数量 + await page.locator('.panel_input input').fill(productNumA); + // 点击搜索 + await page.locator('.panel_input .search_icon').click(); + await numberInput.setValue(BatchQuantityA + 1); + await numberInput.confirmValue(); + + // B 输入数量 + await page.locator('.panel_input input').fill(productNumB); + // 点击搜索 + await page.locator('.panel_input .search_icon').click(); + await numberInput.setValue(BatchQuantityB + 1); + await numberInput.confirmValue(); + + // 获取批次数量列数 + let Batchbill; + const billcolumns = page.locator('.bill_report thead tr th'); + await billcolumns.last().waitFor(); + const headerss = await billcolumns.allInnerTexts(); + Batchbill = headerss.findIndex(headerText => headerText.includes('盈/亏')); + if (Batchbill !== -1) { + console.log(`"盈/亏" 列是第 ${Batchbill + 1} 列`); + } else { + throw new Error('没有找到列'); + } + + // 校验右侧盈亏 + const billLine = page.locator('.bill_report .m-table__body tbody tr'); + const ProfitAndLossA = await billLine + .filter({ has: page.locator('td', { hasText: productA }) }) + .locator('td') + .nth(Batchbill) + .innerText(); + const ProfitAndLossB = await billLine + .filter({ has: page.locator('td', { hasText: productB }) }) + .locator('td') + .nth(Batchbill) + .innerText(); + expect(ProfitAndLossA).toBe('+1'); + expect(ProfitAndLossB).toBe('+1'); + + // 等待 /stock_bill 请求完成并获取流水ID + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/stock_bill') && res.status() === 200; + }), + page.locator('.operate_button', { hasText: '盘点完成' }).click(), + ]); + + const responseBody = await response.json(); + billId = responseBody?.content; + console.log('盘点ID' + '=' + billId); + + // 操作成功 + await page.locator('.ant-message', { hasText: '操作成功' }).waitFor(); + }); + + await test.step('查看入库明细并审核', async () => { + // 根据ID获取水单单号 + const [responses] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/stock_bill?s') && res.status() === 200; + }), + page.locator('.tab_item', { hasText: '出入库管理' }).click(), + ]); + + // 根据ID找到单号 + const responseBody = await responses.json(); + if (Array.isArray(responseBody?.content?.data)) { + let bill = responseBody?.content?.data.find(item => item.id === billId); + billNo = bill?.no; + console.log('单号' + '=' + billNo); + } + + // 根据单号获取该订单行数 + let nowRowA = 0; + const allTrA = page.locator('.m-table__body-wrapper .m-table__body tbody tr'); + const countA = await allTrA.count(); + // 获取单号处于第几行 + for (let i = 0; i < countA; i++) { + const trA = allTrA.nth(i); + const billLine = await trA.locator('.m-table-cell').nth(1).innerText(); + // console.log(billLine) + if (billLine.includes(billNo)) { + nowRowA = i; + break; + } + } + + await page.locator('.m-table__fixed-right .m-table-cell_btn', { hasText: '明细' }).nth(nowRowA).click(); + + // 获取入库数量列数 + let BePutInStorage; + const BPIScolumns = page.locator('.bill_report .m-table__header-wrapper thead th'); + await BPIScolumns.last().waitFor(); + const BPISheaders = await BPIScolumns.allInnerTexts(); + BePutInStorage = BPISheaders.findIndex(headerText => headerText.includes('入库数量')); + if (BePutInStorage !== -1) { + console.log(`"入库数量" 列是第 ${BePutInStorage + 1} 列`); + } else { + throw new Error('没有找到列'); + } + + // 入库列数 + const BpisLine = page.locator('.bill_report .m-table__body-wrapper .m-table__body tbody'); + const BePutInStorageA = await BpisLine.filter({ has: page.locator('td', { hasText: productNumA }) }) + .filter({ has: page.locator('td', { hasText: productA }) }) + .locator('td') + .nth(BePutInStorage) + .innerText(); + const BePutInStorageB = await BpisLine.filter({ has: page.locator('td', { hasText: productNumB }) }) + .filter({ has: page.locator('td', { hasText: productB }) }) + .locator('td') + .nth(BePutInStorage) + .innerText(); + expect(BePutInStorageA).toBe('1'); + expect(BePutInStorageB).toBe('1'); + + await page.locator('.primary_button', { hasText: /^审\s核$/ }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.popup_content')).not.toBeVisible(); + }); + + await test.step('最后再次进入库存余量表校验', async () => { + // 审核后查看产品余量值 + await page.locator('.tab_item', { hasText: '库存管理' }).click(); + ProductMargin = page.locator('.table_sub-box .m-table__body-wrapper .m-table__body tbody tr'); + const ProductMarginedA = await ProductMargin.filter({ + has: page.locator('td', { hasText: productNumA }), + }) + .filter({ has: page.locator('td', { hasText: productA }) }) + .locator('td') + .nth(6) + .innerText(); + const ProductMarginedB = await ProductMargin.filter({ + has: page.locator('td', { hasText: productNumB }), + }) + .filter({ has: page.locator('td', { hasText: productB }) }) + .locator('td') + .nth(6) + .innerText(); + expect(ProductMarginedA).toBe(`${BatchQuantityA + 1}`); + expect(ProductMarginedB).toBe(`${BatchQuantityB + 1}`); + }); + }); + + test('开用盘点', async ({ page, homeNavigation, numberInput }) => { + const productC = ProjectName.Product.Product_5.name; //产品C + const productD = ProjectName.Product.Product_7.name; //产品D + + const productNumC = ProjectName.Product.Product_5.num; //产品C + const productNumD = ProjectName.Product.Product_7.num; //产品D + + // 库存总量 + let inventoryC; + let inventoryD; + + // 产品余量 + let ProductMargin; + + // 获取单号的ID + let billId; + /**@type {string} 单号*/ + let billNo; + + await test.step('记录各自批次数量', async () => { + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).waitFor(); + await page.locator('.tab_item', { hasText: '盘点' }).click(); + await page.locator('.tab_item', { hasText: '盘点' }).locator('.anticon').click(); + await page.locator('.ant-dropdown-menu-item', { hasText: '开用盘点' }).click(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + + // 获取库存总量列数 + let inventory; + const columns = page.locator('.panel_report thead tr th'); + await columns.last().waitFor(); + const headers = await columns.allInnerTexts(); + inventory = headers.findIndex(headerText => headerText.includes('开用余量')); + if (inventory !== -1) { + console.log(`"开用余量" 列是第 ${inventory + 1} 列`); + } else { + throw new Error('没有找到列'); + } + + // 获取各项目库存总量 + const ProductLine = page.locator('.panel_report .m-table__body tbody tr'); + inventoryC = Number( + await ProductLine.filter({ has: page.locator('td', { hasText: productC }) }) + .filter({ has: page.locator('td', { hasText: productNumC }) }) + .locator('td') + .nth(inventory) + .innerText(), + ); + inventoryD = Number( + await ProductLine.filter({ has: page.locator('td', { hasText: productD }) }) + .filter({ has: page.locator('td', { hasText: productNumD }) }) + .locator('td') + .nth(inventory) + .innerText(), + ); + console.log(inventoryC, inventoryD); + }); + + await test.step('输入各产品指定数量并查看', async () => { + // C 输入数量 + await page.locator('.panel_input input').fill(productNumC); + // 点击搜索 + await page.locator('.panel_input .search_icon').click(); + await page.locator('div').filter({ hasText: /^123$/ }).getByRole('button').nth(3).click(); + await numberInput.setValue(inventoryC - 1); + await numberInput.confirmValue(); + + // D 输入数量 + await page.locator('.panel_input input').fill(productNumD); + // 点击搜索 + await page.locator('.panel_input .search_icon').click(); + await numberInput.setValue(inventoryD - 1); + await numberInput.confirmValue(); + + // 获取批次数量列数 + let Batchbill; + const billcolumns = page.locator('.bill_report thead tr th'); + await billcolumns.last().waitFor(); + const headerss = await billcolumns.allInnerTexts(); + Batchbill = headerss.findIndex(headerText => headerText.includes('盈/亏')); + if (Batchbill !== -1) { + console.log(`"盈/亏" 列是第 ${Batchbill + 1} 列`); + } else { + throw new Error('没有找到列'); + } + + // 校验右侧盈亏 + const billLine = page.locator('.bill_report .m-table__body tbody tr'); + const ProfitAndLossC = await billLine + .filter({ has: page.locator('td', { hasText: productC }) }) + .locator('td') + .nth(Batchbill) + .innerText(); + const ProfitAndLossD = await billLine + .filter({ has: page.locator('td', { hasText: productD }) }) + .locator('td') + .nth(Batchbill) + .innerText(); + expect(ProfitAndLossC).toBe('-1'); + expect(ProfitAndLossD).toBe('-1'); + + // 等待 /stock_bill 请求完成并获取流水ID + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/stock_bill') && res.status() === 200; + }), + page.locator('.operate_button', { hasText: '盘点完成' }).click(), + ]); + + const responseBody = await response.json(); + billId = responseBody?.content; + console.log('盘点ID' + '=' + billId); + + // 操作成功 + await page.locator('.ant-message', { hasText: '操作成功' }).waitFor(); + }); + + await test.step('记录库存余量表', async () => { + // 收集初始产品余量值 + await page.locator('.tab_item', { hasText: '库存管理' }).click(); + await page.locator('.ant-switch-checked').waitFor(); + ProductMargin = page.locator('.table_sub-box .m-table__body-wrapper .m-table__body tbody tr'); + await page.locator('.keyword_search input').fill(productNumC); + await page.locator('.keyword_search .ant-input-suffix').click(); + await page.locator('.keyword_search .ant-input-suffix .anticon-close-circle').click(); + await page.locator('.keyword_search input').fill(productNumD); + await page.locator('.keyword_search .ant-input-suffix').click(); + }); + + await test.step('查看出库明细并审核', async () => { + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + // 选择出入库单 + await page.locator('.outInStock_filter .ant-select-selection__rendered').last().click(); + + // 根据ID获取水单单号 + const [responses] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/stock_bill?s') && res.status() === 200; + }), + page.locator('.ant-select-dropdown-menu-item', { hasText: '出库单' }).click(), + ]); + + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + + // 根据ID找到单号 + const responseBody = await responses.json(); + if (Array.isArray(responseBody?.content?.data)) { + let bill = responseBody?.content?.data.find(item => item.id === billId); + billNo = bill?.no; + console.log('单号' + '=' + billNo); + } + + // 根据单号获取该订单行数 + let nowRowA = 0; + const allTrA = page.locator('.m-table__body-wrapper .m-table__body tbody tr'); + const countA = await allTrA.count(); + // 获取单号处于第几行 + for (let i = 0; i < countA; i++) { + const trA = allTrA.nth(i); + const billLine = await trA.locator('.m-table-cell').nth(1).innerText(); + // console.log(billLine) + if (billLine.includes(billNo)) { + nowRowA = i; + break; + } + } + + await page.locator('.m-table__fixed-right .m-table-cell_btn', { hasText: '明细' }).nth(nowRowA).click(); + + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + + // 获取出库数量列数 + let OutboundQuantity; + const OQcolumns = page.locator('.bill_report .m-table__header-wrapper thead th'); + await OQcolumns.last().waitFor(); + const OQheaders = await OQcolumns.allInnerTexts(); + OutboundQuantity = OQheaders.findIndex(headerText => headerText.includes('出库数量')); + if (OutboundQuantity !== -1) { + console.log(`"出库数量" 列是第 ${OutboundQuantity + 1} 列`); + } else { + throw new Error('没有找到列'); + } + + // 出库列数 + const OQLine = page.locator('.bill_report .m-table__body-wrapper .m-table__body tbody'); + const OutboundQuantityC = await OQLine.filter({ has: page.locator('td', { hasText: productNumC }) }) + .filter({ has: page.locator('td', { hasText: productC }) }) + .locator('td') + .nth(OutboundQuantity) + .innerText(); + const OutboundQuantityD = await OQLine.filter({ has: page.locator('td', { hasText: productNumD }) }) + .filter({ has: page.locator('td', { hasText: productD }) }) + .locator('td') + .nth(OutboundQuantity) + .innerText(); + expect(OutboundQuantityC).toBe(1 + ''); + expect(OutboundQuantityD).toBe(1 + ''); + + await page.locator('.primary_button', { hasText: /^审\s核$/ }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.popup_content')).not.toBeVisible(); + }); + + await test.step('最后再次进入库存余量表校验', async () => { + // 审核后查看产品余量值 + await page.locator('.tab_item', { hasText: '库存管理' }).click(); + await page.locator('.keyword_search input').fill(productNumC); + await page.locator('.keyword_search .ant-input-suffix').click(); + const ProductMarginedC = await ProductMargin.filter({ + has: page.locator('td', { hasText: productNumC }), + }) + .filter({ has: page.locator('td', { hasText: productC }) }) + .locator('td') + .nth(7) + .innerText(); + await page.locator('.keyword_search .ant-input-suffix .anticon-close-circle').click(); + await page.locator('.keyword_search input').fill(productNumD); + await page.locator('.keyword_search .ant-input-suffix').click(); + const ProductMarginedD = await ProductMargin.filter({ + has: page.locator('td', { hasText: productNumD }), + }) + .filter({ has: page.locator('td', { hasText: productD }) }) + .locator('td') + .nth(7) + .innerText(); + await page.locator('.keyword_search .ant-input-suffix .anticon-close-circle').click(); + expect(ProductMarginedC).toBe(inventoryC - 1 + ''); + expect(ProductMarginedD).toBe(inventoryD - 1 + ''); + }); + }); + + test('清理产出的数据', async ({ page, homeNavigation }) => { + await test.step('清理产出的数据', async () => { + // 最后恢复数据操作 + await page.reload(); + // 点击库存 + await homeNavigation.gotoModule('库存'); + await page.locator('.tab_item', { hasText: '出入库管理' }).click(); + // 等待加载完毕 + await expect(page.locator('.m-table__icon__warp')).not.toBeVisible(); + await page.locator('.outInStock_filter .ant-select-selection__rendered').last().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '入库单' }).click(); + await page.locator('.m-table__fixed-right .m-table-cell_btn', { hasText: '反审' }).first().click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.outInStock_filter .ant-select-selection__rendered').last().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '出库单' }).click(); + await page.locator('.m-table__fixed-right .m-table-cell_btn', { hasText: '反审' }).first().click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + }); + }); +}); diff --git a/tests/touch/boss_marketing.spec.ts b/tests/touch/boss_marketing.spec.ts new file mode 100644 index 0000000..e1aab3f --- /dev/null +++ b/tests/touch/boss_marketing.spec.ts @@ -0,0 +1,1798 @@ +// @ts-check +import { faker } from '@faker-js/faker/locale/zh_CN'; +import { test, expect } from '@/fixtures/boss_common.js'; +import path from 'path'; +import { decodeQR } from '@/utils/utils.js'; +import { Customer } from '@/utils/customer'; +import { staffData } from '@/fixtures/staff.js'; +import { HomeNavigation } from '@/pages/homeNavigationPage.js'; +import fs from 'fs'; + +test.describe('商城', () => { + test.describe('上架好物', () => { + test('售卖中商品操作', async ({ page, homeNavigation, createCustomer, customerPage }) => { + const code = page.locator('.m-table__body-wrapper .main-table-body_tr'); + + const $goods_f = code.filter({ has: page.locator('td .title', { hasText: '商品F' }) }); + + const customer = createCustomer; + + await test.step('给顾客开会员卡', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.locator('.number_service').waitFor(); + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + // 选择会员卡A + await page + .locator('.memberCard_box > .needsclick') + .getByText(/^会员卡$/) + .click(); + // 结算 + await page.getByRole('button', { name: '去结算' }).click(); + // 选择现金支付 + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + // 关闭收银界面 + await page.locator('use').first().click(); + }); + + await test.step('进入H5商城购买商品商品F', async () => { + await page.goto(process.env.STOREH5_URL ?? ''); + await page.locator('.bar_item', { hasText: '我的' }).click(); + await page.getByText('登录', { exact: true }).click(); + // 登录顾客手机 + await page.locator('.uni-radio-input').click(); //点击同意协议 + await page.locator('.uni-radio-input-checked').waitFor(); + await page + .locator('.login-box_input') + .filter({ + has: page.locator('.input-placeholder', { hasText: '请输入手机号码' }), + }) + .locator('.uni-input-input') + .fill(customer.phone); + await page + .locator('.login-box_input') + .filter({ + has: page.locator('.input-placeholder', { hasText: '请输入验证码' }), + }) + .locator('.uni-input-input') + .fill('1660'); + await expect(async () => { + await page.locator('.login-box_btn', { hasText: /^登录$/ }).click(); + await page.locator('.bar_item', { hasText: '商城' }).waitFor({ timeout: 2000 }); + }).toPass(); + // 进入商城 + await page.locator('.bar_item', { hasText: '商城' }).click(); + // 全部 + await page.locator('.li', { hasText: '全部' }).click(); + await expect(page.locator('.mescroll-wxs-content .pname', { hasText: '商品F' }).first()).toBeVisible(); + // 点击进入商品 + await page.locator('.mescroll-wxs-content .pname', { hasText: '商品F' }).first().click(); + // 购买商品 + await page.locator('.button_hover', { hasText: '立即购买' }).click(); + // 选择门店 + await page.locator('.scroll-view-item').first().waitFor(); + await page.locator('.scroll-view-item', { hasText: 'AT测试一店' }).click(); + await page + .locator('.pickstore-body') + .filter({ has: page.locator('.scroll-view-item', { hasText: 'AT测试一店' }) }) + .locator('.public_color') + .waitFor(); + // 确认 + await page.locator('.mgj-picker-btn-ok', { hasText: /^确认$/ }).click(); + await page.locator('.box-header_content .title', { hasText: '订单支付' }).waitFor(); + // 选择会员卡支付 + await page.locator('.yellow_row', { hasText: '会员卡' }).click(); + await page.locator('.uni-radio-input-checked').waitFor(); + // 确认支付 + await page.locator('.okbtn', { hasText: '确认支付' }).click(); + await expect(page.locator('.okText', { hasText: '支付成功' })).toBeVisible(); + }); + + await test.step('返回上架好物验证', async () => { + await page.goto(process.env.BASE_URL ?? ''); + // 进入营销模块 + await homeNavigation.gotoModule('营销'); + await page.locator('.listBox .name', { hasText: '上架好物' }).click(); + await page.locator('.batch').waitFor(); + + // 进入订单列表 + await $goods_f.locator('.ant-btn', { hasText: '订单列表' }).click(); + await page.locator('.popup_content .title', { hasText: '订单列表' }).waitFor(); + // 验证订单列表中有顾客购买的商品F + await expect( + page.locator('.popup_content .main-table-body_tr').first().locator('td').nth(2), + ).toContainText(customer.phone); + // 关闭订单列表 + await page.locator('.popup_content .close_icon').last().click(); + + // 下架商品 + await $goods_f.locator('.ant-btn', { hasText: '下架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '下架成功' })).toBeVisible(); + // 判断商品下架后不可见 + await expect( + page.locator('.m-table__body-wrapper .main-table-body_tr', { hasText: '商品F' }), + ).not.toBeVisible(); + + // 点击未上架 + await page.locator('.tab_item', { hasText: '未上架' }).click(); + await expect(page.getByText('查询中')).not.toBeVisible(); + // 判断商品As可见 + await expect( + page.locator('.m-table__body-wrapper .main-table-body_tr', { hasText: '商品F' }), + ).toBeVisible(); + + await $goods_f.locator('.ant-btn', { hasText: '上架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '上架成功' })).toBeVisible(); + }); + }); + + test('查询商品', async ({ page, homeNavigation }) => { + await test.step('进入营销上架好物', async () => { + // 进入营销-上架好物 + await homeNavigation.gotoModule('营销'); + await page.locator('.listBox .name', { hasText: '上架好物' }).click(); + await page.locator('.batch').waitFor(); + }); + + await test.step('通过商品分类查询', async () => { + // 点击分类查询健康养生 + await page.locator('.ant-select-selection__rendered', { hasText: '全部分类' }).click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '健康养生' }).click(); + await page.locator('.main-table-body_tr').first().waitFor(); + // 校验健康养生的商品AsI艾经络 + const Health = page.locator('.main-table-body_tr .title', { hasText: 'AI艾经络' }); + await expect(Health).toBeVisible(); + await page.locator('.ant-radio-button-wrapper', { hasText: '卖品类' }).click(); + await page.locator('.ant-radio-button-wrapper', { hasText: '服务类' }).click(); + await page.locator('.ant-select-selection__rendered', { hasText: '全部商品类别' }).waitFor(); + }); + + await test.step('通过商品类别查询', async () => { + // 点击商品类别卖品 + await page.locator('.ant-select-selection__rendered', { hasText: '全部商品类别' }).click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '卖品' }).click(); + await page.locator('.main-table-body_tr').first().waitFor(); + // 校验卖品中精油是否存在 + const EssentialOil = page.locator('.main-table-body_tr .title', { hasText: '精油' }); + await expect(EssentialOil).toBeVisible(); + await page.locator('.ant-radio-button-wrapper', { hasText: '卖品类' }).click(); + await page.locator('.ant-radio-button-wrapper', { hasText: '服务类' }).click(); + await page.locator('.ant-select-selection__rendered', { hasText: '全部商品类别' }).waitFor(); + }); + + await test.step('通过营销方案查询', async () => { + // 点击营销方案 + await page.locator('.ant-select-selection__rendered', { hasText: '全部营销方案' }).click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '团购' }).click(); + await page.locator('.main-table-body_tr').first().waitFor(); + // 校验团购商品Cs存在 + const productC = page.locator('.main-table-body_tr .title', { hasText: '团购商品C' }); + await expect(productC).toBeVisible(); + await page.locator('.ant-radio-button-wrapper', { hasText: '卖品类' }).click(); + await page.locator('.ant-radio-button-wrapper', { hasText: '服务类' }).click(); + await page.locator('.ant-select-selection__rendered', { hasText: '全部商品类别' }).waitFor(); + }); + + await test.step('通过关键字查询', async () => { + // 通过关键字搜索 + await page.getByPlaceholder('关键字搜索').fill('肌底清洁'); + await page.locator('.ant-input-prefix').first().click(); + await page.locator('.main-table-body_tr').first().waitFor(); + const keyword = page.locator('.main-table-body_tr .title', { hasText: '肌底清洁' }); + await expect(keyword).toBeVisible(); + }); + }); + + test('商品系列', async ({ page, homeNavigation }) => { + const code = page.locator('.main-table-body_tr').first(); + + await test.step('进入营销上架好物', async () => { + // 进入营销-上架好物 + await homeNavigation.gotoModule('营销'); + await page.locator('.listBox .name', { hasText: '上架好物' }).click(); + await page.locator('.batch').waitFor(); + // 点击商品系列 + await page.locator('.ant-radio-button-wrapper', { hasText: '商品系列' }).click(); + await page.locator('.ant-btn-lg').first().waitFor(); + // 点击新增 + await page.locator('.ant-btn-lg', { hasText: /^新\s增$/ }).click(); + await page.locator('.header', { hasText: '新增商品系列' }).waitFor(); + // 关闭 这里的关闭是因为不进行一些操作 后面的内容不加载 强行点了一下新增并关掉 + await page.locator('.box .top .anticon').click(); + + // 循环把商品系列都删了 + const del = page.locator('.main-table-body_tr'); + const dels = await del.count(); + if (await del.first().isVisible()) { + console.log('有' + dels + '个待删除'); + for (let i = 0; i < dels; i++) { + await page + .locator('.main-table-body_tr') + .nth(i) + .locator('.ant-btn-link', { hasText: '删除' }) + .click(); + await page.locator('.content', { hasText: '确认删除该商品系列吗?' }).waitFor(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message', { hasText: '删除成功' }).waitFor(); + } + await expect(del).not.toBeVisible(); + } + }); + + await test.step('新增', async () => { + // 点击新增 + await page.locator('.ant-btn-lg', { hasText: /^新\s增$/ }).click(); + await page.locator('.header', { hasText: '新增商品系列' }).waitFor(); + // 输入系列名称 + // await page.locator('.ant-form-item-children .ant-input').fill('测试商品A') + await page.getByPlaceholder('长度不大于10个字符').fill('测试商品A'); + // 点击添加商品 + await page.locator('.addBtn').click(); + await page.locator('.popup_content .title', { hasText: '选择商品' }).waitFor(); + // 选择商品 + await page.locator('.customWith').first().click(); + const productA = await page.locator('.customWith').first().innerText(); + await page.locator('.customWith').nth(1).click(); + const productB = await page.locator('.customWith').nth(1).innerText(); + // 确认选择 + await page.locator('.comfirm_btn', { hasText: '确认选择' }).click(); + await page.locator('.ant-radio-wrapper').last().click(); + // 确认添加 + await page.locator('.submit_btn', { hasText: '确认添加' }).click(); + await page.locator('.ant-message').waitFor(); + await expect(page.locator('.header', { hasText: '新增商品系列' })).not.toBeVisible(); + // 校验新增数据准确性 + + const NameA = await code.locator('td').first().innerText(); + const productName = await code.locator('td').nth(2).innerText(); + const productBName = await code.locator('td').nth(3).innerText(); + expect(NameA).toBe('测试商品A'); + const regex = new RegExp(`^(${productB}、${productA}|${productA}、${productB})$`); + expect(productName).toMatch(regex); + expect(productBName).toBe(productB); + }); + + await test.step('编辑', async () => { + // 点击编辑 + await page.locator('.main-table-body_tr').first().locator('.ant-btn-link', { hasText: '编辑' }).click(); + await page.locator('.header', { hasText: '编辑商品系列' }).waitFor(); + // 修改系列名称 + await page.getByPlaceholder('长度不大于10个字符').fill('测试商品B'); + // 确认保存 + await page.locator('.submit_btn', { hasText: '确认保存' }).click(); + await page.locator('.ant-message').waitFor(); + await expect(page.locator('.header', { hasText: '编辑商品系列' })).not.toBeVisible(); + const NameB = await code.locator('td').first().innerText(); + expect(NameB).toBe('测试商品B'); + }); + + await test.step('删除', async () => { + await page.locator('.main-table-body_tr').first().locator('.ant-btn-link', { hasText: '删除' }).click(); + await page.locator('.content', { hasText: '确认删除该商品系列吗?' }).waitFor(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '删除成功' })).toBeVisible(); + await expect(code.locator('td').first()).not.toBeVisible(); + }); + }); + + test('商品属性', async ({ page, homeNavigation }) => { + const code = page.locator('.main-table-body_tr').first(); + + await test.step('进入营销上架好物', async () => { + // 进入营销-上架好物 + await homeNavigation.gotoModule('营销'); + await page.locator('.listBox .name', { hasText: '上架好物' }).click(); + await page.locator('.batch').waitFor(); + // 点击商品系列 + await page.locator('.ant-radio-button-wrapper', { hasText: '商品属性' }).click(); + await page.locator('.ant-btn-lg').first().waitFor(); + // 点击新增 + await page.locator('.ant-btn-lg', { hasText: /^新\s增$/ }).click(); + await page.locator('.header', { hasText: '新增商品属性' }).waitFor(); + // 关闭 + await page.locator('.box .top .anticon').click(); + + // 循环把商品属性删了 + const del = page.locator('.main-table-body_tr'); + const dels = await del.count(); + if (await del.first().isVisible()) { + console.log('有' + dels + '个待删除'); + for (let i = 0; i < dels; i++) { + await page + .locator('.main-table-body_tr') + .nth(i) + .locator('.ant-btn-link', { hasText: '删除' }) + .click(); + await page.locator('.content', { hasText: '确认删除该商品属性吗?' }).waitFor(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '删除成功' })).toBeVisible(); + } + await expect(del).not.toBeVisible(); + } + }); + + await test.step('新增', async () => { + // 点击新增 + await page.locator('.ant-btn-lg', { hasText: /^新\s增$/ }).click(); + await page.locator('.header', { hasText: '新增商品属性' }).waitFor(); + // 输入系列名称 + await page.getByPlaceholder('长度不大于10个字符').first().fill('测试商品属性'); + // 点击添加商品属性 + await page.locator('.ant-btn', { hasText: '添加属性' }).click(); + const quantity = await page.locator('.del_icon').count(); + expect(quantity).toBe(2); + // 点击添加商品属性 + await page.locator('.ant-btn', { hasText: '添加属性' }).click(); + const quantitys = await page.locator('.del_icon').count(); + expect(quantitys).toBe(3); + await page.getByPlaceholder('长度不大于10个字符').nth(1).fill('商品属性A'); + await page.getByPlaceholder('长度不大于10个字符').nth(2).fill('商品属性B'); + await page.getByPlaceholder('长度不大于10个字符').nth(3).fill('商品属性C'); + await page.locator('.submit_btn', { hasText: /^添\s加$/ }).click(); + await page.locator('.ant-message').waitFor(); + await expect(page.locator('.header', { hasText: '新增商品属性' })).not.toBeVisible(); + // 校验新增数据准确性 + const NameA = await code.locator('td').first().innerText(); + const productName = await code.locator('td').nth(1).innerText(); + expect(NameA).toBe('测试商品属性'); + expect(productName).toBe('商品属性A、 商品属性B、 商品属性C'); + }); + + await test.step('编辑', async () => { + // 点击编辑 + await page.locator('.main-table-body_tr').first().locator('.ant-btn-link', { hasText: '编辑' }).click(); + await page.locator('.header', { hasText: '编辑商品属性' }).waitFor(); + // 删除两个商品属性 + await page.locator('.del_icon').last().click(); + const quantitys = await page.locator('.del_icon').count(); + expect(quantitys).toBe(2); + await page.locator('.del_icon').last().click(); + const quantity = await page.locator('.del_icon').count(); + expect(quantity).toBe(0); + // 确认保存 + await page.locator('.submit_btn', { hasText: /^保\s存$/ }).click(); + await page.locator('.ant-message').waitFor(); + await expect(page.locator('.header', { hasText: '编辑商品属性' })).not.toBeVisible(); + const productNameB = await code.locator('td').nth(1).innerText(); + expect(productNameB).toBe('商品属性A'); + }); + + await test.step('删除', async () => { + await page.locator('.main-table-body_tr').first().locator('.ant-btn-link', { hasText: '删除' }).click(); + await page.locator('.content', { hasText: '确认删除该商品属性吗?' }).waitFor(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '删除成功' })).toBeVisible(); + await expect(code.locator('td').first()).not.toBeVisible(); + }); + }); + }); + + test.describe('上架好物', () => { + test.beforeEach(async ({ page, homeNavigation }) => { + // 下架所有测试商品 + await homeNavigation.gotoModule('营销'); + await page.locator('.listBox .name', { hasText: '上架好物' }).click(); + await page.locator('.batch').waitFor(); + await expect(page.locator('.no_data')).not.toBeVisible(); + + // 商品列表定位器 + const code = page.locator('.m-table__body-wrapper .main-table-body_tr'); + const goodsList = ['商品As', '商品B', '商品D', '商品Cs', '商品H', '商品M']; + for (const goods of goodsList) { + const commodity = code.filter({ has: page.locator('td .title', { hasText: goods }) }).first(); + if (await commodity.isVisible()) { + await commodity.locator('.ant-btn', { hasText: '下架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '下架成功' })).toBeVisible(); + } + } + }); + + test('未上架商品操作', async ({ page, homeNavigation }) => { + // 商品列表定位器 + const code = page.locator('.m-table__body-wrapper .main-table-body_tr'); + // 各商品显示路径 + const commodityA = code.filter({ has: page.locator('td .title', { hasText: '商品As' }) }); + const commodityB = code.filter({ has: page.locator('td .title', { hasText: '商品B' }) }); + const commodityD = code.filter({ has: page.locator('td .title', { hasText: '商品D' }) }); + const commodityCS = code.filter({ has: page.locator('td .title', { hasText: '商品Cs' }) }); + const commodityH = code.filter({ has: page.locator('td .title', { hasText: '商品H' }) }); + const commodityM = code.filter({ has: page.locator('td .title', { hasText: '商品M' }) }); + + await test.step('返回上架好物验证', async () => { + // 跳转查询中消失 + const noData = page.locator('.no_data'); + const Element = page.locator('.ant-btn-primary', { hasText: /^新\s增$/ }); + + // 进入营销模块 + await page.goto(process.env.BASE_URL ?? ''); + await homeNavigation.gotoModule('营销'); + await page.locator('.listBox .name', { hasText: '上架好物' }).click(); + await page.locator('.batch').waitFor(); + + // 上架商品As上架 + await page.locator('.tab_item', { hasText: '未上架' }).click(); + await expect(Element).toBeVisible(); + await expect(noData).not.toBeVisible(); + await commodityA.locator('.ant-btn', { hasText: '上架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '上架成功' })).toBeVisible(); + + // 下架商品As + await page.locator('.tab_item', { hasText: '售卖中' }).click(); + await expect(noData).not.toBeVisible(); + await expect(commodityA).toBeVisible(); + await commodityA.locator('.ant-btn', { hasText: '下架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '下架成功' })).toBeVisible(); + + // 进入未上架 + await page.locator('.tab_item', { hasText: '未上架' }).click(); + await expect.soft(noData).not.toBeVisible(); + await expect(Element).toBeVisible(); + + await expect.soft(commodityB).toBeVisible(); + await expect(commodityCS).toBeVisible(); + + // 获取商品B 商品Cs行数 + const cede = page.locator('.m-table__body-wrapper .main-table-body_tr'); + const productBLine = await cede + .locator('td .title') + .allInnerTexts() + .then(text => { + return text.findIndex(item => item.includes('商品B')); + }); + const productCLine = await cede + .locator('td .title') + .allInnerTexts() + .then(text => { + return text.findIndex(item => item.includes('商品Cs')); + }); + if (productBLine === -1 || productCLine === -1) { + throw new Error('商品B或商品Cs不存在未上架商品中'); + } + + // 批量操作上架商品B 商品Cs + const check = page.locator('.m-table-fixed-body tbody tr'); + await check.nth(productBLine).locator('td').nth(0).click(); + await check.nth(productBLine).locator('.ant-checkbox-wrapper-checked').waitFor(); + await check.nth(productCLine).locator('td').nth(0).click(); + await check.nth(productCLine).locator('.ant-checkbox-wrapper-checked').waitFor(); + await page.locator('.batch_span', { hasText: '批量操作' }).click(); + await page.locator('.ant-dropdown-menu-item', { hasText: '上架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '上架成功' })).toBeVisible(); + + // 售卖中存在商品B 商品Cs + await page.locator('.tab_item', { hasText: '售卖中' }).click(); + await expect(noData).not.toBeVisible(); + await expect(commodityB).toBeVisible(); + await expect(commodityCS).toBeVisible(); + + // 下架商品B + await commodityB.locator('.ant-btn', { hasText: '下架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '下架成功' })).toBeVisible(); + // 下架商品Cs + await commodityCS.locator('.ant-btn', { hasText: '下架' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '下架成功' })).toBeVisible(); + + // 进入未上架 + await page.locator('.tab_item', { hasText: '未上架' }).click(); + await expect.soft(Element).toBeVisible(); + await expect(noData).not.toBeVisible(); + + // 复制商品D,并且改名为商品H + await commodityD.locator('.ant-btn', { hasText: '复制' }).click(); + const service = page.locator('.header', { hasText: '新增服务' }); + await expect(service).toBeVisible(); + await page.getByPlaceholder('长度不大于40个字符').fill('商品H'); + await page.locator('.ant-btn-primary', { hasText: /^保\s存$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '保存成功' })).toBeVisible(); + await expect(commodityH).toBeVisible(); + + // 删除商品H + await commodityH.locator('.ant-btn', { hasText: '删除' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message', { hasText: '删除成功' }).waitFor(); + + //新增商品M + await page.locator('.input', { hasText: /^新\s增$/ }).click(); + await expect.soft(service).toBeVisible(); + await page.getByPlaceholder('长度不大于40个字符').fill('商品M'); + // 选择商品分类 + await page + .locator('.ant-row') + .filter({ has: page.locator('.ant-form-item-required', { hasText: '商品分类' }) }) + .locator('.ant-col-19 .ant-select-enabled') + .last() + .click(); + await page.locator('.ant-select-dropdown-menu-item').first().click(); + await page + .locator('.ant-row') + .filter({ has: page.locator('.ant-form-item-required', { hasText: '商品售价' }) }) + .getByPlaceholder('保留两位小数') + .fill('99'); + await page + .locator('.ant-row') + .filter({ has: page.locator('.ant-form-item-required', { hasText: '门店原价' }) }) + .getByPlaceholder('保留两位小数') + .fill('19'); + await page + .locator('.ant-row') + .filter({ has: page.locator('.ant-form-item-required', { hasText: '最大可售卖数' }) }) + .locator('.ant-input') + .fill('999'); + // 添加全部门店 + await page.locator('.addShopBtn', { hasText: '添加可销售门店' }).click(); + await expect(page.locator('.comSelect_title', { hasText: /^选择$/ })).toBeVisible(); + await page.getByRole('button', { name: /全\s选/ }).click(); + await page.getByRole('button', { name: '确定选择' }).first().click(); + await page.locator('.ant-btn-primary', { hasText: '下一步' }).click(); + // 允许使用会员卡金支付 + await page.getByLabel('允许使用会员卡金支付').check(); + await page.getByLabel('所有').check(); + await page.locator('.ant-btn-primary', { hasText: '下一步' }).click(); + await page.locator('.ant-row .upload_icon').last().click(); + // 上传图片 + const path1 = './tests/imgs/商品M.jpg'; + await page + .locator('span') + .filter({ hasText: '我已阅读并同意 《慧来客图片上传协议》' }) + .getByRole('textbox') + .setInputFiles(path1); + await page.locator('.ant-btn', { hasText: '开始上传' }).click(); + await expect(page.locator('.ant-message', { hasText: '上传成功' })).toBeVisible(); + await page.locator('.ant-upload-list-item-list-type-picture-card').waitFor(); + await page.locator('.ant-btn-primary', { hasText: /^保\s存$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '保存成功' })).toBeVisible(); + // 校验商品M + await expect(commodityM).toBeVisible(); + + // 删除商品M + await commodityM.locator('.ant-btn', { hasText: '删除' }).click(); + await page.locator('.comfirm_btn', { hasText: /^确\s认$/ }).click(); + await page.locator('.ant-message', { hasText: '删除成功' }).waitFor(); + + await page.locator('.close_btn > .icon > svg').click(); + await expect(page.locator('.close_btn > .icon > svg')).not.toBeVisible(); + }); + }); + }); + + test.describe('商城订单', () => { + test('查询订单', async ({ page, homeNavigation, createCustomer, customerPage }) => { + const customer = createCustomer; + + await test.step('给顾客进行购买会员卡', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + // 选择会员 + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.locator('.number_service').waitFor(); + // 点击开卡 + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + // 选择会员卡A + await page + .locator('.memberCard_box > .needsclick') + .getByText(/^会员卡$/) + .click(); + // 结算 + await page.getByRole('button', { name: '去结算' }).click(); + // 选择现金支付 + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + await test.step('进入H5商城购买商品', async () => { + await page.goto(process.env.STOREH5_URL ?? ''); + await page.locator('.bar_item', { hasText: '我的' }).click(); + await page.getByText('登录', { exact: true }).click(); + // 登录顾客手机 + // 点击同意协议 + await page.locator('.uni-radio-input').click(); + await page.locator('.uni-radio-input-checked').waitFor(); + await page + .locator('.login-box_input') + .filter({ + has: page.locator('.input-placeholder', { hasText: '请输入手机号码' }), + }) + .locator('.uni-input-input') + .fill(customer.phone); + await page + .locator('.login-box_input') + .filter({ + has: page.locator('.input-placeholder', { hasText: '请输入验证码' }), + }) + .locator('.uni-input-input') + .fill('1660'); + await expect(async () => { + await page.locator('.login-box_btn', { hasText: /^登录$/ }).click(); + await page.locator('.bar_item', { hasText: '商城' }).waitFor(); + }).toPass(); + // 进入商城 + await page.locator('.bar_item', { hasText: '商城' }).click(); + // 全部 + await page.locator('.li', { hasText: '全部' }).click(); + // 点击进入商品 + await page.locator('.mescroll-wxs-content .pname', { hasText: '商品F' }).first().click(); + // 购买商品 + await page.locator('.button_hover', { hasText: '立即购买' }).click(); + // 选择门店 + await page.locator('.scroll-view-item').first().waitFor(); + await page.locator('.scroll-view-item', { hasText: 'AT测试一店' }).click(); + await page + .locator('.pickstore-body') + .filter({ has: page.locator('.scroll-view-item', { hasText: 'AT测试一店' }) }) + .locator('.public_color') + .waitFor(); + // 确认 + await page.locator('.mgj-picker-btn-ok', { hasText: /^确认$/ }).click(); + await page.locator('.box-header_content .title', { hasText: '订单支付' }).waitFor(); + // 选择会员卡支付 + await page.locator('.yellow_row', { hasText: '会员卡' }).click(); + await page.locator('.uni-radio-input-checked').waitFor(); + // 确认支付 + await page.locator('.okbtn', { hasText: '确认支付' }).click(); + await expect(page.locator('.okText', { hasText: '支付成功' })).toBeVisible(); + // 返回 + await page.locator('.back').click(); + // 购买商品 + await page.locator('.button_hover', { hasText: '立即购买' }).click(); + // 选择门店 + await page.locator('.scroll-view-item').first().waitFor(); + await page.locator('.scroll-view-item', { hasText: 'AT测试一店' }).click(); + await page + .locator('.pickstore-body') + .filter({ has: page.locator('.scroll-view-item', { hasText: 'AT测试一店' }) }) + .locator('.public_color') + .waitFor(); + // 确认 + await page.locator('.mgj-picker-btn-ok', { hasText: /^确认$/ }).click(); + await page.locator('.box-header_content .title', { hasText: '订单支付' }).waitFor(); + // 选择微信支付 + await page.locator('.form_row .wechat').click(); + await page.locator('.uni-radio-input-checked').waitFor(); + + // 确认支付 + await page.locator('.okbtn', { hasText: '确认支付' }).click(); + await expect(async () => { + const Yes = page.locator('.uni-modal__btn_primary', { hasText: '是' }); + if (await Yes.isVisible()) { + await page.locator('.uni-modal__btn_primary', { hasText: '是' }).click(); + await page.locator('.okbtn', { hasText: '确认支付' }).click(); + } + await expect(Yes).not.toBeVisible(); + // 购买一个套餐F 自动核销为已使用 + await page.locator('.jump_btn', { hasText: '回到商家' }).last().waitFor({ timeout: 5000 }); //不限制会等30秒 + await page.locator('.jump_btn', { hasText: '回到商家' }).last().click(); + }).toPass(); + + // 进入商城 + await page.locator('.bar_item', { hasText: '商城' }).click(); + // 全部 + await page.locator('.li', { hasText: '全部' }).click(); + // 点击进入商品 + await page.locator('.mescroll-wxs-content .pname', { hasText: '套餐F' }).first().click(); + // 购买商品 + await page.locator('.button_hover', { hasText: '立即购买' }).click(); + // 选择门店 + await page.locator('.scroll-view-item').first().waitFor(); + await page.locator('.scroll-view-item', { hasText: 'AT测试一店' }).click(); + await page + .locator('.pickstore-body') + .filter({ has: page.locator('.scroll-view-item', { hasText: 'AT测试一店' }) }) + .locator('.public_color') + .waitFor(); + // 确认 + await page.locator('.mgj-picker-btn-ok', { hasText: /^确认$/ }).click(); + await page.locator('.box-header_content .title', { hasText: '订单支付' }).waitFor(); + // 选择会员卡支付 + await page.locator('.yellow_row', { hasText: '会员卡' }).click(); + await page.locator('.uni-radio-input-checked').waitFor(); + // 确认支付 + await page.locator('.okbtn', { hasText: '确认支付' }).click(); + await expect.soft(page.locator('.okText', { hasText: '支付成功' })).toBeVisible(); + }); + + await test.step('回商城订单根据状态校验', async () => { + await page.goto(process.env.BASE_URL ?? ''); + // 进入营销 + await homeNavigation.gotoModule('营销'); + // 点击商城订单 + await page.locator('.listBox .name', { hasText: '商城订单' }).click(); + await page.locator('.top .title', { hasText: '商城' }).waitFor(); + // 勾选已使用 + await expect(async () => { + await page.locator('.ant-checkbox-wrapper', { hasText: '未使用' }).click(); + const SelectA = page.locator('.ant-checkbox-wrapper-checked', { hasText: '未使用' }); + await expect(SelectA).not.toBeVisible({ timeout: 2000 }); + }).toPass(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 判断刚创的会员有已使用套餐 + const code = page.locator('.scroller .list .detail'); + const HaveBeenUsed = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '套餐F' }) }) + .filter({ has: page.locator('.status_text', { hasText: '已使用' }) }); + await expect(HaveBeenUsed).toBeVisible(); + + // 勾选未使用 + await expect(async () => { + await page.locator('.ant-checkbox-wrapper', { hasText: '未使用' }).click(); + const SelectA = page.locator('.ant-checkbox-wrapper-checked', { hasText: '未使用' }); + await expect(SelectA).toBeVisible({ timeout: 2000 }); + await page.locator('.ant-checkbox-wrapper', { hasText: '已使用' }).click(); + const SelectC = page.locator('.ant-checkbox-wrapper-checked', { hasText: '已使用' }); + await expect(SelectC).not.toBeVisible({ timeout: 2000 }); + }).toPass(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 判断刚创的会员有未使用商品 + const NotInUse = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '商品F' }) }) + .filter({ has: page.locator('.status_text', { hasText: '未使用' }) }); + await expect(NotInUse).toBeVisible(); + + // 勾选未支付 + await expect(async () => { + await page.locator('.ant-checkbox-wrapper', { hasText: '未支付' }).click(); + const SelectB = page.locator('.ant-checkbox-wrapper-checked', { hasText: '未支付' }); + await expect(SelectB).toBeVisible({ timeout: 2000 }); + await page.locator('.ant-checkbox-wrapper', { hasText: '未使用' }).click(); + const SelectC = page.locator('.ant-checkbox-wrapper-checked', { hasText: '未使用' }); + await expect(SelectC).not.toBeVisible({ timeout: 2000 }); + }).toPass(); + + // 判断刚创的会员有待支付 + const NonPayment = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '商品F' }) }) + .filter({ has: page.locator('.status_text', { hasText: '待支付' }) }); + await expect(NonPayment).toBeVisible(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 除了退款全勾上 + await expect(async () => { + await page.locator('.ant-checkbox-wrapper', { hasText: '未支付' }).click(); + const SelectB = page.locator('.ant-checkbox-wrapper-checked', { hasText: '未支付' }); + await expect(SelectB).toBeVisible({ timeout: 2000 }); + await page.locator('.ant-checkbox-wrapper', { hasText: '未使用' }).click(); + const SelectA = page.locator('.ant-checkbox-wrapper-checked', { hasText: '未使用' }); + await expect(SelectA).toBeVisible({ timeout: 2000 }); + await page.locator('.ant-checkbox-wrapper', { hasText: '已使用' }).click(); + const SelectC = page.locator('.ant-checkbox-wrapper-checked', { hasText: '已使用' }); + await expect(SelectC).toBeVisible({ timeout: 2000 }); + }).toPass(); + }); + + await test.step('根据类型校验', async () => { + await page.locator('.ant-select-selection__rendered', { hasText: '选择商品类型' }).click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '服务类' }).click(); + const ServiceClass = page.locator('.ant-select-selection-selected-value', { hasText: '服务类' }); + await ServiceClass.waitFor(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 判断刚创的会员有已使用套餐 + const code = page.locator('.scroller .list .detail'); + const HaveBeenUsed = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '套餐F' }) }) + .filter({ has: page.locator('.status_text', { hasText: '已使用' }) }); + await expect(HaveBeenUsed).toBeVisible(); + + // 去掉筛选条件 + await page.locator('.anticon-close-circle').click(); + await expect(ServiceClass).not.toBeVisible(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await page.locator('.ant-select-selection__rendered', { hasText: '选择商品分类' }).click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '美容养颜' }).click(); + const CommodityType = page.locator('.ant-select-selection-selected-value', { hasText: '美容养颜' }); + await expect(CommodityType).toBeVisible(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 判断刚创的会员有已使用套餐 + await expect(HaveBeenUsed).toBeVisible(); + + // 去掉筛选条件 + await page.locator('.anticon-close-circle').click(); + await expect(CommodityType).not.toBeVisible(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await page.locator('.ant-select-selection--multiple', { hasText: '选择使用门店' }).click(); + await page.locator('.comSelect_title', { hasText: '选择使用门店' }).click(); + // 选择一店 + await page.locator('.customWith', { hasText: 'AT测试一店' }).click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + // 判断刚创的会员有已使用套餐 + await expect(HaveBeenUsed).toBeVisible(); + await page.locator('.ant-select-selection__choice__remove').click(); + }); + + await test.step('根据搜索校验', async () => { + // 点击搜索 + await page.locator('.searchButton').click(); + await page.getByPlaceholder('搜索关键字').fill('套餐F'); + await page.locator('.ant-input-affix-wrapper-input-with-clear-btn .search_btn').click(); + + // 判断刚创的会员有已使用套餐 + const code = page.locator('.scroller .list .detail'); + const HaveBeenUsed = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '套餐F' }) }) + .filter({ has: page.locator('.status_text', { hasText: '已使用' }) }); + await expect(HaveBeenUsed).toBeVisible(); + }); + }); + + test('订单详情', async ({ page, homeNavigation, createCustomer, customerPage }) => { + const code = page.locator('.scroller .list .detail'); + + const customer = createCustomer; + + await test.step('开会员卡用于消费商品', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.locator('.number_service').waitFor(); + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + // 选择会员卡A + await page + .locator('.memberCard_box > .needsclick') + .getByText(/^会员卡$/) + .click(); + // 结算 + await page.getByRole('button', { name: '去结算' }).click(); + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + await test.step('进入H5商城购买商品', async () => { + await page.goto(process.env.STOREH5_URL ?? ''); + await page.locator('.bar_item', { hasText: '我的' }).click(); + await page.getByText('登录', { exact: true }).click(); + // 登录顾客手机 + await page.locator('.uni-radio-input').click(); //点击同意协议 + await page.locator('.uni-radio-input-checked').waitFor(); + await page + .locator('.login-box_input') + .filter({ + has: page.locator('.input-placeholder', { hasText: '请输入手机号码' }), + }) + .locator('.uni-input-input') + .fill(customer.phone); + await page + .locator('.login-box_input') + .filter({ + has: page.locator('.input-placeholder', { hasText: '请输入验证码' }), + }) + .locator('.uni-input-input') + .fill('1660'); + await expect(async () => { + await page.locator('.login-box_btn', { hasText: /^登录$/ }).click(); + await page.locator('.bar_item', { hasText: '商城' }).waitFor(); + }).toPass({ timeout: 60000 }); + // 进入商城 + await page.locator('.bar_item', { hasText: '商城' }).click(); + // 全部 + await page.locator('.li', { hasText: '全部' }).click(); + // 点击进入商品 + await page.locator('.mescroll-wxs-content .pname', { hasText: '商品E' }).first().click(); + // 购买商品 + await page.locator('.button_hover', { hasText: '立即购买' }).click(); + // 选择门店 + await page.locator('.scroll-view-item').first().waitFor(); + await page.locator('.scroll-view-item', { hasText: 'AT测试一店' }).click(); + await page + .locator('.pickstore-body') + .filter({ has: page.locator('.scroll-view-item', { hasText: 'AT测试一店' }) }) + .locator('.public_color') + .waitFor(); + // 确认 + await page.locator('.mgj-picker-btn-ok', { hasText: /^确认$/ }).click(); + await page.locator('.box-header_content .title', { hasText: '订单支付' }).waitFor(); + // 选择会员卡支付 + await page.locator('.yellow_row', { hasText: '会员卡' }).click(); + await page.locator('.uni-radio-input-checked').waitFor(); + // 确认支付 + await page.locator('.okbtn', { hasText: '确认支付' }).click(); + await expect(page.locator('.okText', { hasText: '支付成功' })).toBeVisible(); + }); + + await test.step('搜索商品E验证', async () => { + await page.goto(process.env.BASE_URL ?? ''); + // 进入营销 + await homeNavigation.gotoModule('营销'); + // 点击商城订单 + await page.locator('.listBox .name', { hasText: '商城订单' }).click(); + await page.locator('.top .title', { hasText: '商城' }).waitFor(); + + // 点击搜索 + await page.locator('.searchButton').click(); + await page.getByPlaceholder('搜索关键字').fill('商品E'); + await page.locator('.ant-input-affix-wrapper-input-with-clear-btn .search_btn').click(); + + // 判断会员买的商品E 数量1 实付金额100元,状态未使用 + const Member = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '商品E' }) }) + .filter({ has: page.locator('.status_text', { hasText: '未使用' }) }) + .filter({ has: page.locator('.num', { hasText: '1' }) }) + .filter({ has: page.locator('.amount', { hasText: '¥100' }) }); + await expect(Member).toBeVisible(); + }); + + await test.step('点击顾客名称进入顾客详情', async () => { + // 点击顾客名称 + await page.locator('.scroller .list .detail .user', { hasText: customer.username }).click(); + await page.locator('.consume_data').waitFor(); + // 对比该顾客是不是刚创建的 + await expect.soft(page.locator('.user_name')).toContainText(customer.username); + // await expect(page.getByText(customer.phone)).toBeVisible(); + await expect(page.locator('.member_info_box .phone')).toContainText(customer.phone); + // 关闭顾客详情 + await page.locator('.close_icons').click(); + const BasicData = page.locator('.ant-tabs-tab-active', { hasText: '基本资料' }); + await expect(BasicData).not.toBeVisible(); + }); + + await test.step('点击备注,输入备注并保存', async () => { + const remark = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '商品E' }) }) + .filter({ has: page.locator('.status_text', { hasText: '未使用' }) }) + .filter({ has: page.locator('.num', { hasText: '1' }) }) + .filter({ has: page.locator('.amount', { hasText: '¥100' }) }); + await expect(remark).toBeVisible(); + + // 点击备注 并输入备注 + await remark.locator('.comment .anticon').click(); + await page.locator('.title', { hasText: '编辑备注' }).waitFor(); + await page.getByPlaceholder('请输入1-100个字符备注内容').click(); // 点击输入框触发保存 + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('测试'); + await page.locator('.comfirm_btn', { hasText: /^保\s存$/ }).click(); + + // 判断刚输入的备注是否成功 + await expect(remark.locator('.comment')).toContainText('测试'); + }); + + await test.step('点击详情,查看商品购买详情', async () => { + const Details = code + .filter({ has: page.locator('.head', { hasText: customer.username }) }) + .filter({ has: page.locator('div .name', { hasText: '商品E' }) }) + .filter({ has: page.locator('.status_text', { hasText: '未使用' }) }) + .filter({ has: page.locator('.num', { hasText: '1' }) }) + .filter({ has: page.locator('.amount', { hasText: '¥100' }) }); + // 点击详情 + await Details.locator('.ant-btn-link', { hasText: '详情' }).click(); + await expect(page.locator('.header', { hasText: '订单详情' })).toBeVisible(); + + // 判断会员手机、订单状态、支付方式、实付金额 + await expect.soft(page.locator('.info_warp .phone')).toContainText(customer.phone); + await expect.soft(page.locator('.box').getByText('未使用')).toBeVisible(); + await expect.soft(page.locator('.alloytouch-target .item').locator('div').nth(9)).toContainText('卡金'); + await expect(page.locator('.alloytouch-target .item').locator('div').nth(19)).toContainText('100'); + }); + }); + + test.skip('导出', async ({ page, homeNavigation }) => { + // 进入营销 + await homeNavigation.gotoModule('营销'); + // 点击商城订单 + await page.locator('.listBox .name', { hasText: '商城订单' }).click(); + await page.locator('.top .title', { hasText: '商城' }).waitFor(); + const downloadPromise = page.waitForEvent('download'); + // 点击更多 + await page.locator('.more .batch_span').click(); + // 点击导出 + await page.locator('.ant-dropdown-menu-item', { hasText: '导出' }).click(); + const download = await downloadPromise; + // 获取下载的文件名 + const fileName = download.suggestedFilename(); + // 指定保存下载文件的路径 + const downloadPath = path.join(__dirname, `@/imgs/${fileName}`); + // 保存下载的文件 + await download.saveAs(downloadPath); + // 验证文件是否成功保存 + const fileExists = fs.existsSync(downloadPath); + expect(fileExists).toBeTruthy(); + }); + }); + + test('抢购', async ({ page, homeNavigation }) => { + // 进入营销-抢购 + await homeNavigation.gotoModule('营销'); + await page.locator('.category_1_itemStyle .category_1_itemStyle_content_name', { hasText: /^抢购$/ }).click(); + await page.locator('.main-table-body_tr').first().waitFor(); + const GroupBuying = await page.locator('.ant-select-selection-selected-value').innerText(); + expect(GroupBuying).toBe('抢购'); + }); + + test('团购', async ({ page, homeNavigation }) => { + // 进入营销-团购 + await homeNavigation.gotoModule('营销'); + await page.locator('.category_1_itemStyle .category_1_itemStyle_content_name', { hasText: /^团购$/ }).click(); + await page.locator('.main-table-body_tr').first().waitFor(); + const GroupBuying = await page.locator('.ant-select-selection-selected-value').innerText(); + expect(GroupBuying).toBe('团购'); + }); +}); + +test.describe('品牌', () => { + test.describe('门店信息', () => { + test('品牌信息配置', async ({ page, homeNavigation }) => { + await homeNavigation.gotoModule('营销'); + await page.getByText('个性装扮').click(); + + // 随机门店名称 + const marketingName = 'AT测试' + faker.string.alpha(3); + // 上传图片路径 + const uploadImgFile = path.join(__dirname, '@/imgs/upload.jpg'); + + const $marketingNameInput = page + .locator('div') + .filter({ has: page.getByText('品牌名称') }) + .locator('input[type="text"]'); + const $uploadBtn = page.locator('input[type="file"]'); + const $uploadImg = page + .locator('div') + .filter({ has: page.getByText('品牌logo') }) + .locator('.upload_img img'); + + await $marketingNameInput.fill(marketingName); + await page.getByLabel('icon: delete').locator('svg').click(); + await $uploadImg.focus(); + await expect($uploadBtn).toBeVisible(); + await $uploadBtn.setInputFiles(uploadImgFile); + const [res] = await Promise.all([ + page.waitForResponse(res => res.url().includes('/base64Upload') && res.status() === 200), + page.getByRole('button', { name: '开始上传' }).click(), + ]); + const resBody = await res.json(); + const getImgPath = resBody?.content?.filePath; + expect(getImgPath).not.toBeNull(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + await page.locator('label').filter({ hasText: '门店信息' }).click(); + await page.locator('label').filter({ hasText: '品牌信息' }).click(); + + await expect($marketingNameInput).toHaveValue(marketingName); + await expect($uploadImg.filter({ hasText: getImgPath })).toBeVisible(); + }); + + test.skip('门店信息配置,预览修改配置', async ({ page, homeNavigation }) => { + const storeName = 'AT测试一店' + faker.string.alpha(3); + const storeTag = '一店' + faker.string.alpha(3); + const storePhone = faker.helpers.fromRegExp(/1[3-9][0-1]{9}/); + + const $firstStoreTr = page.locator('.main-table-body_tr').first(); + + const $popupIframe = page.locator('.popup_content iframe').contentFrame(); + + await test.step('进入门店信息', async () => { + await homeNavigation.gotoModule('营销'); + await page.getByText('个性装扮').click(); + await page.locator('span').filter({ hasText: '门店信息' }).click(); + await expect(page.getByText('门店排序')).toBeVisible(); + }); + + await test.step('门店信息配置', async () => { + const $storeName = page.locator('.item', { hasText: '门店名称' }).locator('.ant-input'); + const $storeTag = page.locator('.item', { hasText: '区域标签' }).locator('.ant-input'); + const $storePhone = page.locator('.item', { hasText: '电话' }).locator('.ant-input'); + + await $firstStoreTr.getByText('编辑').first().click(); + await $storeName.fill(storeName); + await $storeTag.fill(storeTag); + await $storePhone.fill(storePhone); + await page.locator('.tangram-suggestion tr > td').first().click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + + await expect(page.getByRole('cell', { name: storeName })).toBeVisible(); + await expect(page.getByRole('cell', { name: storeTag })).toBeVisible(); + await expect(page.getByRole('cell', { name: storePhone })).toBeVisible(); + }); + + await test.step('预览修改配置', async () => { + await $firstStoreTr.getByText('扫码预览').first().click(); + await $popupIframe.locator('.bar_item', { hasText: '预约' }).click(); + await $popupIframe.getByText('切换门店').click(); + await expect($popupIframe.getByText(storeName)).toBeVisible(); + }); + }); + + test('门店排序', async ({ page, homeNavigation }) => { + let firstTrStoreName; + let secondTrStoreName; + + await test.step('进入门店信息', async () => { + await homeNavigation.gotoModule('营销'); + await page.getByText('个性装扮').click(); + await page.locator('span').filter({ hasText: '门店信息' }).click(); + await expect(page.getByText('门店排序')).toBeVisible(); + }); + + await test.step('门店排序,查看门店排序', async () => { + await page.getByText('门店排序').click(); + const $popup = page.locator('.popup_content'); + const $firstTr = $popup.locator('.sort_tbody .tr').nth(0); + const $secondTr = $popup.locator('.sort_tbody .tr').nth(1); + + firstTrStoreName = await $firstTr.locator('.item').first().innerText(); + secondTrStoreName = await $secondTr.locator('.item').first().innerText(); + + // await page.pause(); + await $secondTr.locator('.item').nth(1).dragTo($firstTr.locator('.item').nth(1)); + await expect($firstTr.locator('.item').first()).toContainText(secondTrStoreName); + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + + const $$storeTr = page.locator('.main-table-body_tr'); + await expect($$storeTr.nth(0).getByRole('cell').nth(1)).toContainText(secondTrStoreName); + await expect($$storeTr.nth(1).getByRole('cell').nth(1)).toContainText(firstTrStoreName); + }); + }); + }); + + test.describe('个性化', () => { + test('模块', async ({ page, homeNavigation }) => { + let createModuleName = '新增模块名称' + faker.helpers.fromRegExp(/[0-9]{2}/); + let editModuleName = '修改模块名称' + faker.helpers.fromRegExp(/[0-9]{2}/); + const sideIframe = page.frameLocator('.side iframe'); + const popupIframe = page.locator('.popup_content iframe').contentFrame(); + let tableTr = page.locator('.tr'); + let lastTableTr = tableTr.last(); + + await test.step('新增模块', async () => { + await homeNavigation.gotoModule('营销'); + + await page.getByText('个性装扮').click(); + await page.getByText('个性化').click(); + await page.getByRole('button', { name: '新增模块' }).click(); + + // 输入模块信息 + await page.getByPlaceholder('输入名称').fill(createModuleName); + await page.getByText('选择图标').click(); + await page + .locator('div') + .filter({ hasText: /^分析$/ }) + .first() + .click(); + await lastTableTr + .locator('div') + .filter({ hasText: /^选择页面$/ }) + .click(); + await page.locator('tr', { hasText: '商城' }).getByLabel('', { exact: true }).check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await page.locator('.pre-table-body').getByText('确认').click(); + + await page.getByRole('button', { name: /保\s存/ }).click(); + + await expect(async () => { + await sideIframe.locator('.bar_item', { hasText: createModuleName }).click(); + await expect(sideIframe.locator('uni-view.title', { hasText: createModuleName })).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + }); + + await test.step('修改模块名称,隐藏模块', async () => { + await page + .locator('div') + .filter({ hasText: new RegExp(`^${createModuleName}$`) }) + .locator('svg') + .click(); + await page.getByPlaceholder('输入名称').fill(editModuleName); + await page.getByRole('button', { name: /保\s存/ }).click(); + + // 查看修改后的模块名称 + await expect(sideIframe.locator('.bar_item', { hasText: editModuleName })).toBeVisible(); + + // 模块隐藏和展示成功 + await page.locator('.tr', { hasText: editModuleName }).getByRole('switch', { name: '是' }).click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await page.locator('.ant-message-notice', { hasText: '操作成功' }).waitFor(); + await expect(sideIframe.locator('.bar_item', { hasText: editModuleName })).not.toBeVisible(); + + await page.locator('.tr', { hasText: editModuleName }).getByRole('switch', { name: '否' }).click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await page.locator('.ant-message-notice', { hasText: '操作成功' }).waitFor(); + await expect(sideIframe.locator('.bar_item', { hasText: editModuleName })).toBeVisible(); + }); + + await test.step('模块排序,删除模块', async () => { + const editTableTr = tableTr.filter({ hasText: editModuleName }); + // 模块排序 + await page + .locator('div') + .filter({ hasText: /^模块排序$/ }) + .nth(1) + .click(); + await editTableTr.locator('.td.td-opt').dragTo(tableTr.nth(1).locator('.td.td-opt')); + + await page.getByText('结束排序').click(); + + await page.locator('.ant-message', { hasText: '排序成功' }).waitFor(); + await page.getByRole('button', { name: /保\s存/ }).click(); + + // await page.pause(); + + const barItemText = await sideIframe.locator('.bar_item').allInnerTexts(); + expect(barItemText[0]).toContain(editModuleName); + + // 模块删除 + await editTableTr.getByText('删除').click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.locator('.ant-message', { hasText: '删除模块成功' }).waitFor(); + await expect(sideIframe.locator('.bar_item', { hasText: editModuleName })).not.toBeVisible(); + await expect(editTableTr).not.toBeVisible(); + + // 判断预览 + await page.getByRole('button', { name: '保存并预览' }).click(); + await expect(popupIframe.locator('.bar_item').last()).toBeVisible(); + // 二维码能够解析出url + const dirPath = path.resolve(__dirname, '@/tests/imgs'); + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + const filePath = path.resolve(dirPath, 'previewWindow.jpg'); + + await page.locator('.qrcode .hqrcode').screenshot({ path: filePath }); + const urlContent = decodeQR(filePath); + expect(urlContent).not.toBeNull(); + }); + }); + + test('个性化配置', async ({ page, homeNavigation }) => { + const $setAppointment = page.locator('.tr').filter({ + has: page.getByText('预约').first(), + }); + + await test.step('进入门店信息', async () => { + await homeNavigation.gotoModule('营销'); + await page.getByText('个性装扮').click(); + await page.getByText('个性化').click(); + await expect(page.getByRole('button', { name: '新增模块' })).toBeVisible(); + await $setAppointment.getByText('个性化').click(); + await expect(page.getByText('个性化设置')).toBeVisible(); + await page.getByLabel('员工优先模式选择员工>选择项目>选择时间>确认预约').check(); + const $box = page.locator('.box'); + await $box.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + await expect(page.getByText('个性化设置')).not.toBeVisible(); + }); + + await test.step('查看配置生效', async () => { + const $sideIframe = page.locator('.side iframe').contentFrame(); + await $sideIframe.getByText('预约', { exact: true }).click(); + await expect($sideIframe.getByText('不指定人预约')).toBeVisible(); + }); + }); + + /**@type { import("@playwright/test").Page } 初始化页面*/ + let initPage; + test.beforeAll(async ({ browser, baseURL }) => { + initPage = await browser.newPage(); + const homeNavigation = new HomeNavigation(initPage); + await initPage.goto(baseURL ?? ''); + await homeNavigation.gotoModule('营销'); + + // 恢复个性化配置 + await initPage.getByText('个性装扮').click(); + await initPage.getByText('个性化').click(); + await test.step('重置预约个性化配置', async () => { + const $setAppointment = initPage.locator('.tr').filter({ + has: initPage.getByText('预约').first(), + }); + await expect($setAppointment).toBeVisible(); + await $setAppointment.getByText('个性化').click(); + await expect(initPage.getByText('个性化设置')).toBeVisible(); + await initPage.getByLabel('项目优先模式选择项目>选择时间>选择员工>确认预约').check(); + const $box = initPage.locator('.box'); + await $box.getByRole('button', { name: /保\s存/ }).click(); + }); + }); + + test.afterAll(async ({ baseURL }) => { + const homeNavigation = new HomeNavigation(initPage); + await initPage.goto(baseURL ?? ''); + await homeNavigation.gotoModule('营销'); + + // 删除所有新增模块 + await initPage.getByText('个性装扮').click(); + await initPage.getByText('个性化').click(); + await initPage + .locator('div') + .filter({ hasText: /^我的$/ }) + .last() + .waitFor(); + const deleteCount = async () => await initPage.locator('.td', { hasText: '删除' }).count(); + const maxRetry = 10; + let retry = 0; + while ((await deleteCount()) > 0 && retry < maxRetry) { + await initPage.locator('.td').getByText('删除').first().click(); + await initPage.getByRole('button', { name: /确\s认/ }).click(); + await initPage.waitForLoadState('domcontentloaded'); + retry++; + } + }); + }); + + test.describe('个性化', () => { + test('配色', async ({ page, homeNavigation }) => { + let colorArray; + + await test.step('进入个性化装扮', async () => { + await homeNavigation.gotoModule('营销'); + await page.getByText('个性装扮').click(); + await page.getByText('个性化').click(); + await page.locator('.main-head span').filter({ hasText: '配色' }).click(); + }); + + await test.step('获取所有主题色', async () => { + const colorItems = await page.locator('.color-item a:first-child').all(); + colorArray = await Promise.all( + colorItems.map(async item => { + return await item.evaluate(el => getComputedStyle(el).backgroundColor); + }), + ); + expect(colorArray.length).not.toBe(0); + }); + + await test.step('设置主题色,并且查看预览色', async () => { + const randomColor = faker.number.int({ min: 0, max: colorArray.length - 1 }); + const $$colorItem = page.locator('.color-item'); + await $$colorItem.nth(randomColor).click(); + await page.getByRole('button', { name: '保存并预览' }).click(); + + const $popupIframe = page.locator('.popup_content iframe').contentFrame(); + const $logo = $popupIframe.locator('.bar_item', { hasText: '我的' }).locator('.logo'); + + await $logo.waitFor(); + await page.waitForTimeout(2000); + const logoColor = await $logo.evaluate(e => getComputedStyle(e).color); + expect(logoColor).toBe(colorArray[randomColor]); + }); + }); + + test('页面', async ({ page, homeNavigation }) => { + const pageName = '测试页面' + faker.string.alpha(3); + const pageContent = '测试页面'; + const copyPageName = pageName + '复制'; + const $tableTr = page.locator('.main-table-body_tr'); + const $newPageTr = $tableTr.filter({ + has: page.locator('td:first-child').filter({ hasText: new RegExp(`^${pageName}$`) }), + }); + const $copyPageTr = $tableTr.filter({ + has: page.locator('td:first-child').filter({ hasText: new RegExp(`^${copyPageName}$`) }), + }); + const $storePageTr = $tableTr.filter({ + has: page.locator('td:first-child').filter({ hasText: '商城' }), + }); + + await homeNavigation.gotoModule('营销'); + + await test.step('进入品牌-个性化-页面', async () => { + await page.getByText('个性装扮').click(); + await page.getByText('个性化').click(); + await page.locator('.main-head span').filter({ hasText: '页面' }).click(); + }); + + await test.step('创建页面', async () => { + await page.getByRole('button', { name: '创建页面' }).click(); + await page.getByText('选择模板').click(); + const $$viewList = page.locator('.temp-view'); + await $$viewList.first().focus(); + await expect($$viewList.first().getByText('复制此模板')).toBeVisible(); + await $$viewList.first().getByText('复制此模板').click(); + await page.locator('.add-btn').first().click(); + await page.getByText('公告栏').click(); + await page.getByPlaceholder('请输入公告内容').fill(pageContent); + await page.getByPlaceholder('自定义页面名称').fill(pageName); + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect.soft($newPageTr).toBeVisible(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + }); + + await test.step('应用新建页面到商城模块', async () => { + await $newPageTr.locator('td').nth(2).locator('svg').click(); + const $popup = page.locator('.popup_content'); + const index = await page + .locator('.popup_content .main-table-body_tr') + .allInnerTexts() + .then(text => { + return text.findIndex(el => el.includes('商城')); + }); + + const $popupTr = $popup.locator('.m-table__fixed-left').locator('tr').nth(index); + await $popupTr.getByRole('checkbox').check(); + await expect($popupTr.getByRole('checkbox')).toBeChecked(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(page.getByRole('button', { name: /确\s认/ })).not.toBeVisible(); + }); + + await test.step('保存并预览页面', async () => { + await page.getByText('模块', { exact: true }).click(); + await page.getByRole('button', { name: '保存并预览' }).click(); + const $popupIframe = page.locator('.popup_content iframe').contentFrame(); + await $popupIframe.locator('.bar_item', { hasText: '商城' }).click(); + await expect.soft($popupIframe.getByText(pageContent).first()).toBeVisible(); + await page.locator('.close-btn > svg').click(); + }); + + await test.step('复制新建页面', async () => { + await page.locator('.main-head span').filter({ hasText: '页面' }).click(); + await $newPageTr.getByText('复制').click(); + await page.getByPlaceholder('自定义页面名称').fill(copyPageName); + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect.soft($copyPageTr).toBeVisible(); + }); + + await test.step('重置页面应用,并且删除页面', async () => { + await $storePageTr.locator('td').nth(2).locator('svg').click(); + const $popup = page.locator('.popup_content'); + const index = await page + .locator('.popup_content .main-table-body_tr') + .allInnerTexts() + .then(text => { + return text.findIndex(el => el.includes('商城')); + }); + const $popupTr = $popup.locator('.m-table__fixed-left').locator('tr').nth(index); + await $popupTr.getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(page.getByRole('button', { name: /确\s认/ })).not.toBeVisible(); + + await $newPageTr.getByText('删除').click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect.soft($newPageTr).not.toBeVisible(); + + await $copyPageTr.getByText('删除').click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect.soft($copyPageTr).not.toBeVisible(); + }); + }); + }); +}); + +test.describe('邀客系统', () => { + test('礼品设置', async ({ page, homeNavigation, marketingPage, marketingInviteGuestsPage }) => { + const giftName = '新增礼品' + faker.string.alpha(3); + const editGiftName = giftName + '编辑'; + const copyGiftName = giftName + '复制'; + + const $$pickerItem = page.locator('.com_picker').locator('div > label'); + + const $bodyTable = page.locator('.m-table__body-wrapper'); + const $fixedLeftTable = page.locator('.m-table__fixed-left'); + + const $giftTr = $bodyTable + .locator('.main-table-body_tr') + .filter({ has: page.getByRole('cell', { name: giftName, exact: true }) }); + const $editGiftTr = $bodyTable + .locator('.main-table-body_tr') + .filter({ has: page.getByRole('cell', { name: editGiftName, exact: true }) }); + const $copyGiftTr = $bodyTable + .locator('.main-table-body_tr') + .filter({ has: page.getByRole('cell', { name: copyGiftName, exact: true }) }); + + await test.step('礼品设置', async () => { + await homeNavigation.gotoModule('营销'); + await marketingPage.gotoStrategySubPage('邀客系统'); + await marketingInviteGuestsPage.gotoSubPage('设置'); + await marketingInviteGuestsPage.gotoSettingSubPage('礼品设置'); + }); + + await test.step('新增礼品', async () => { + await page.getByRole('button', { name: '新增礼品' }).click(); + + await page.getByPlaceholder('长度不大于20个字符').fill(giftName); + + await page.getByRole('button', { name: '添加项目' }).click(); + await expect(async () => { + await $$pickerItem.first().getByRole('checkbox').uncheck(); + await $$pickerItem.first().getByRole('checkbox').check(); + await expect(page.locator('span').filter({ hasText: '全部' }).locator('span')).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + const $$serviceItem = page.locator('.item', { hasText: '体验项目' }).locator('.drag-item'); + // 输入选择的体验项目有效期 + await $$serviceItem.first().getByRole('textbox').nth(1).fill('3'); + + await page.getByRole('button', { name: '确认保存' }).click(); + + await expect($giftTr).toBeVisible(); + }); + + await test.step('编辑礼品', async () => { + await $giftTr.getByText('编辑').click(); + await page.getByPlaceholder('长度不大于20个字符').fill(editGiftName); + await page.getByRole('button', { name: '添加项目' }).click(); + await expect(async () => { + await $$pickerItem.nth(2).getByRole('checkbox').uncheck(); + await $$pickerItem.nth(2).getByRole('checkbox').check(); + await expect(page.locator('span').filter({ hasText: '全部' }).locator('span')).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + const $$serviceItem = page.locator('.item', { hasText: '体验项目' }).locator('.drag-item'); + // 输入选择的体验项目有效期 + await $$serviceItem.nth(1).getByRole('textbox').nth(1).fill('3'); + + await page.getByRole('button', { name: '确认保存' }).click(); + + await expect($editGiftTr).toBeVisible(); + }); + + await test.step('复制礼品', async () => { + await $editGiftTr.getByText('复制').click(); + await page.getByPlaceholder('长度不大于20个字符').fill(copyGiftName); + await page.getByRole('button', { name: '确认保存' }).click(); + await expect($copyGiftTr).toBeVisible(); + }); + + await test.step('批量删除礼品', async () => { + const $$selectGift = $fixedLeftTable.locator('tbody td[colspan="1"]'); + const allGiftName = await page.locator('.main-table-body_tr td:nth-child(2)').allInnerTexts(); + const editGiftIndex = allGiftName.findIndex(e => e === editGiftName); + const copyGiftIndex = allGiftName.findIndex(e => e === copyGiftName); + await $$selectGift.nth(editGiftIndex).locator('input').check(); + await $$selectGift.nth(copyGiftIndex).locator('input').check(); + + await page.getByText('批量操作').click(); + await page.getByRole('menuitem', { name: '批量删除' }).click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + await expect($editGiftTr).not.toBeVisible(); + await expect($copyGiftTr).not.toBeVisible(); + }); + }); + + test('礼品分配', async ({ page, homeNavigation, marketingPage, marketingInviteGuestsPage }) => { + const firstStoreEmployee = staffData.firstStore.firstSector.employee_1; + page.locator('.m-table__header-wrapper'); + const $bodyTable = page.locator('.m-table__body-wrapper'); + page.locator('.m-table__fixed-left'); + const $firstStoreEmployeeTr = $bodyTable + .locator('.main-table-body_tr') + .filter({ has: page.getByRole('cell', { name: firstStoreEmployee.name }) }) + .filter({ has: page.getByText('顾问') }); + const $margin = $firstStoreEmployeeTr.locator('td').nth(4); + + await test.step('进入礼品分配页面', async () => { + await homeNavigation.gotoModule('营销'); + await marketingPage.gotoStrategySubPage('邀客系统'); + await marketingInviteGuestsPage.gotoSubPage('设置'); + await marketingInviteGuestsPage.gotoSettingSubPage('礼品分配'); + }); + + await test.step('选择一店员工进行分配礼品', async () => { + await expect(async () => { + await page.locator('.store_box a').click(); + await expect(page.locator('.com_picker').getByLabel('AT测试一店')).toBeVisible({ timeout: 2000 }); + }).toPass(); + await page.getByLabel('AT测试一店').check(); + + const lastValue = Number(await $margin.innerText()); + + await $firstStoreEmployeeTr.getByText('分配').click(); + await page.locator('.gift_item').first().getByRole('checkbox').check(); + await page.getByRole('button', { name: '保存' }).click(); + await page.getByRole('button', { name: '确认添加' }).click(); + await page.locator('.ant-message', { hasText: '操作成功' }).waitFor(); + await expect(page.getByRole('button', { name: '确认添加' })).not.toBeVisible(); + const value = Number(await $margin.innerText()); + + expect(value - lastValue).toBe(1); + }); + }); + + test('返利方案配置', async ({ page, homeNavigation, createCustomer, marketingPage, marketingInviteGuestsPage }) => { + const customer = createCustomer; + + const rebateSchemeName = '新建返利方案' + faker.string.alpha(3); + page.locator('.m-table__header-wrapper'); + const $bodyTable = page.locator('.m-table__body-wrapper'); + page.locator('.m-table__fixed-left'); + const $$inputBalance = page.locator('.public_list').filter({ hasText: '返余额' }).locator('input'); + const $$inputIntegral = page.locator('.public_list').filter({ hasText: '返积分' }).locator('input'); + // 项目选择 + const $$pickerItem = page.locator('.com_picker').locator('div > label'); + + await test.step('进入管理,将该顾客设置为邀客', async () => { + await homeNavigation.gotoModule('营销'); + await marketingPage.gotoStrategySubPage('邀客系统'); + await marketingInviteGuestsPage.gotoSubPage('邀客管理'); + await page.getByRole('button', { name: '添加邀客' }).click(); + await page.getByRole('button', { name: '手动添加邀客' }).click(); + await page + .locator('.popup_content') + .getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索') + .fill(customer.phone); + await page.locator('.popup_content').getByText('搜索').click(); + await page.getByRole('row', { name: customer.phone }).getByLabel('').check(); + await page.getByRole('button', { name: '确认选择' }).click(); + await page.getByRole('button', { name: '确认添加' }).click(); + await expect(page.getByText('导入成功')).toBeVisible(); + await page.getByRole('button', { name: /确\s认/ }).click(); + }); + + await test.step('新建返利方案', async () => { + await marketingInviteGuestsPage.gotoSubPage('返利方案'); + await page.getByRole('button', { name: '创建方案' }).click(); + await page.getByPlaceholder('长度不大于20个字符').fill(rebateSchemeName); + await page.getByRole('checkbox', { name: '通用返利' }).check(); + await $$inputBalance.nth(0).fill('10'); + await $$inputBalance.nth(1).fill('5'); + await $$inputIntegral.nth(0).fill('10'); + await $$inputIntegral.nth(1).fill('5'); + + await page.getByRole('checkbox', { name: '指定项目' }).check(); + await page.getByRole('button', { name: '新增' }).click(); + await expect(async () => { + await $$pickerItem.nth(2).getByRole('checkbox').uncheck(); + await $$pickerItem.nth(2).getByRole('checkbox').check(); + await expect(page.locator('span').filter({ hasText: '全部' }).locator('span')).toBeVisible({ + timeout: 2000, + }); + }).toPass(); + await page.getByRole('button', { name: '确定选择' }).click(); + + await page.getByText('卖品', { exact: true }).first().click(); + await page.getByRole('checkbox', { name: '通用返利' }).check(); + await $$inputBalance.nth(0).fill('10'); + await $$inputBalance.nth(1).fill('5'); + await $$inputIntegral.nth(0).fill('10'); + await $$inputIntegral.nth(1).fill('5'); + + await page.getByRole('button', { name: /保\s存/ }).click(); + + await expect(page.getByRole('cell', { name: rebateSchemeName })).toBeVisible(); + }); + + await test.step('为新建返利方案配置邀客', async () => { + await page.getByRole('row', { name: rebateSchemeName }).locator('svg').first().click(); + await page.getByRole('button', { name: '添加邀客' }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + + await expect(async () => { + await page.getByText('搜索', { exact: true }).click(); + await expect(page.getByRole('row', { name: customer.phone })).toBeVisible({ timeout: 2000 }); + }).toPass(); + + await page.getByRole('row', { name: customer.phone }).getByLabel('').check(); + await page.getByRole('button', { name: '确认选择' }).click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + + const $rebateScheme = $bodyTable.locator('.main-table-body_tr').filter({ hasText: rebateSchemeName }); + + await expect($rebateScheme).toContainText('1'); + + await page.getByRole('row', { name: rebateSchemeName }).locator('svg').first().click(); + + await page.getByText('移除').first().click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + + await page.getByRole('row', { name: rebateSchemeName }).getByText('删除').click(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect(page.locator('.ant-message', { hasText: '操作成功' })).toBeVisible(); + // 关闭邀客管理页面 + await page.locator('.top').filter({ hasText: '邀客系统' }).locator('.close_btn').click(); + await expect(page.locator('.top').filter({ hasText: '邀客系统' })).not.toBeVisible(); + }); + }); + + test('邀客管理', async ({ page, homeNavigation, tablePage, customerPage, createCustomers, marketingPage }) => { + /**@type {Customer[]} */ + let customers = []; + await test.step('创建顾客', async () => { + customers = await createCustomers(2); + }); + + await test.step('进入邀客管理页面,添加邀客', async () => { + await homeNavigation.gotoModule('营销'); + await marketingPage.gotoStrategySubPage('邀客系统'); + + await page.getByRole('button', { name: '添加邀客' }).click(); + await page.getByRole('button', { name: '手动添加邀客' }).click(); + + const $selectCustomerView = page.locator('.popup_content'); + // 批量手动添加邀客 + for (const customer of customers) { + await $selectCustomerView.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await $selectCustomerView.getByText('搜索', { exact: true }).click(); + await $selectCustomerView.getByRole('row', { name: customer.phone }).getByLabel('').check(); + } + + await page.getByRole('button', { name: '确认选择' }).click(); + await page.getByRole('button', { name: '确认添加' }).click(); + await expect(page.getByText('导入成功')).toBeVisible(); + await page.getByRole('button', { name: /确\s认/ }).click(); + }); + + const $$bodyTrTable = tablePage.bodyTrTable; + const $firstCustomerTr = $$bodyTrTable.filter({ hasText: customers[0].phone }); + const $secondCustomerTr = $$bodyTrTable.filter({ hasText: customers[1].phone }); + + await test.step('搜索邀客B,将邀客A设置为邀客B的上级', async () => { + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customers[1].phone); + await page.getByText('搜索', { exact: true }).click(); + await $secondCustomerTr.getByText('详情').click(); + + // 设置上级 + const $settingLastCustomer = page.locator('.left_box .list_', { hasText: '上级' }); + await $settingLastCustomer.locator('svg').click(); + const $selectCustomerView = page.locator('.popup_content'); + await $selectCustomerView.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customers[0].phone); + await $selectCustomerView.getByText('搜索', { exact: true }).click(); + await $selectCustomerView.getByRole('row', { name: customers[0].phone }).getByLabel('').click(); + await expect($settingLastCustomer).toContainText(customers[0].username); + + // 关闭详情页 + await page.locator('.top').filter({ hasText: '邀客详情' }).locator('.close_btn').click(); + // 关闭邀客管理页面 + await page.locator('.top').filter({ hasText: '邀客系统' }).locator('.close_btn').click(); + }); + + const project = { num: '100020', name: '青春雅致套', price: 1980 }; + await test.step('邀客B购买项目', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(customers[1].phone); + await customerPage.selectSearchCustomer(customers[1].phone); + await page.locator('.project_list .number', { hasText: project.num }).click(); + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + await leftPaymentInfoItem.filter({ hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await expect(page.getByRole('button', { name: /开\s单/ })).toBeVisible(); + }); + + await test.step('搜索邀客A,查看邀客A的下级,进入邀客A详情查看好友数、收益', async () => { + await homeNavigation.gotoModule('营销'); + await marketingPage.gotoStrategySubPage('邀客系统'); + + // 搜索邀客A + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customers[0].phone); + await page.getByText('搜索', { exact: true }).click(); + // 展开邀客A + await $firstCustomerTr.locator('td').nth(0).click(); + await expect.soft($firstCustomerTr).toBeVisible(); + await expect.soft($secondCustomerTr).toBeVisible(); + + // 邀客A详情 + await $firstCustomerTr.getByText('详情').click(); + const $friendsNum = page.locator('.left_box .list', { hasText: 'TA的好友' }); + await expect.soft($friendsNum).toContainText('1'); + + // 邀客A收益 + const $income = page.locator('.left_box .list', { hasText: '收益' }); + await expect.soft($income).toContainText(`${project.price * 0.1}`); + // 关闭详情页 + await page.locator('.top').filter({ hasText: '邀客详情' }).locator('.close_btn').click(); + // 关闭邀客管理页面 + await page.locator('.top').filter({ hasText: '邀客系统' }).locator('.close_btn').click(); + await expect(page.locator('.exit_btn')).toBeVisible(); + }); + }); +}); diff --git a/tests/touch/boss_report.spec.ts b/tests/touch/boss_report.spec.ts new file mode 100644 index 0000000..8c66a56 --- /dev/null +++ b/tests/touch/boss_report.spec.ts @@ -0,0 +1,1657 @@ +// @ts-check +import { expect, test } from '@/fixtures/merchantFixture.js'; +// import { test, expect } from '@/fixtures/boss_common.js'; +import { faker } from '@faker-js/faker/locale/zh_CN'; +import { Customer } from '@/utils/customer'; +import { KeepOnlyNumbers, waitSpecifyApiLoad } from '@/utils/utils.js'; +import { CardType, Employees, ProjectName } from '@/fixtures/userconfig.js'; + +test.describe('业绩明细表', () => { + test('数据校验', async ({ page, createCustomer, homeNavigation, reportPage, customerPage }) => { + // 项目名称 + const projectA = ProjectName.Projects.Projects_17.name; + const projectB = ProjectName.Projects.Projects_18.name; + const projectC = ProjectName.Projects.Projects_1.name; + // 套餐 + const SetMeal = ProjectName.SetMeal.SetMeal_5.name; + // 卖品 + const Product = ProjectName.Product.Product_4.name; + // 充值卡 + const cardname = CardType.CardTypeList.CardType_5.name; + + let ca; + const randomNumA = faker.helpers.fromRegExp(/1[0-9]{10}/); + const randomNumB = faker.helpers.fromRegExp(/1[0-9]{10}/); + + // 创建顾客 + ca = createCustomer; + // 获取姓名、手机号、档案号 + const usernameA = ca.username; + const phoneA = ca.phone; + // 员工 + const employee1 = Employees.FirstShop.Employee_6.name; + + await test.step('购买会员卡B', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await customerPage.searchCustomer(phoneA); + await customerPage.selectSearchCustomer(usernameA); + await page.locator('.number_service').waitFor(); + // 点击开卡,选择会员卡B + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + await page.getByText('会员卡B', { exact: true }).click(); + // 结算,选择现金支付 + await page.getByRole('button', { name: '去结算' }).click(); + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + await test.step('购买并且消耗一次项目,然后购买一个套餐', async () => { + await page.reload(); + // 点击顾客 + await homeNavigation.gotoModule('顾客'); + // 输入信息 + await customerPage.searchCustomer(phoneA); + await customerPage.selectSearchCustomer(usernameA); + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: usernameA }).last().click(); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 购买项目A + await page.getByText(ProjectName.Projects.Projects_17.num).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 消耗该项目1次 + await page.locator('.commodity_item').last().click(); + // 点击添加员工 + await page.locator('.use_item .staff_btn').click(); + // 选择员工A + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 选择套餐 + await page + .locator('.float_tab') + .filter({ hasText: /套\s餐/ }) + .click(); + // 选择第一个套餐 + await page.locator('.project_one > .item').filter({ hasText: ProjectName.SetMeal.SetMeal_5.name }).click(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + // 点击修改单号 + await page.locator('.input165').click(); + // 输入随机单号 + await page.getByPlaceholder('请输入内容').fill(randomNumA); + // 确认 + await page.locator('.tools_icon').last().click(); + // 结算,选择现金支付 + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + await test.step('充值1000元卡金', async () => { + // 输入金额 + const TopUp = '1000'; + + // 点击顾客 + await page.reload(); + await homeNavigation.gotoModule('顾客'); + // 输入信息 + await customerPage.searchCustomer(phoneA); + await customerPage.selectSearchCustomer(usernameA); + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: usernameA }).last().click(); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 点击充值 + await page.getByRole('button', { name: /^充\s值$/ }).click(); + // 点击充值总额 + await page.locator('span > .touchIcon').first().click(); + await page.getByPlaceholder('请输入内容').fill(TopUp); + // 确认金额(勾勾) + await page.locator('.tools_icon').last().click(); + // 点击修改单号 + await page.locator('.input165').click(); + // 输入随机单号 + await page.getByPlaceholder('请输入内容').fill(randomNumB); + // 确认 + await page.locator('.tools_icon').last().click(); + // 结算,选择现金支付 + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + // 关闭收银界面 + await page.locator('use').first().click(); + }); + + await test.step('进入业绩明细表进行校验数据', async () => { + // 项目套餐卖品价格 + const projectPriceA = Number(ProjectName.Projects.Projects_17.Price); + const projectPriceB = Number(ProjectName.Projects.Projects_18.Price); + const projectPriceC = Number(ProjectName.Projects.Projects_1.Price); + const SetMealPriceTC = Number(ProjectName.SetMeal.SetMeal_5.Price); + const ProductPrice = Number(ProjectName.Product.Product_4.Price); + const cardPrice = Number(CardType.CardTypeList.CardType_5.Price); + + // 进入业绩明细表 + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('业绩明细表'); + + // 项目销售记录遍历公用定位 + const cede1 = page.locator('.popup_content .m-table__body-wrapper tbody .main-table-body_tr'); + + // 打开项目销售记录窗口 + await page.locator('.m-table-footer .handleNumber').first().click(); + await page.locator('.popup_content .title', { hasText: '项目销售记录' }).waitFor(); + // 项目A(项目名称、单号、员工)定位器 + const $project_a = cede1 + .filter({ hasText: projectA }) + .filter({ hasText: randomNumA }) + .filter({ has: page.getByText(employee1) }); + // 数量、金额、现金业绩 + await expect.soft($project_a.locator('.m-table-cell').nth(6)).toContainText('1'); + await expect.soft($project_a.locator('.m-table-cell').nth(7)).toContainText(`${projectPriceA}`); + await expect($project_a.locator('.m-table-cell').nth(8)).toContainText(`${projectPriceA * 0.8}`); + + // 项目B(项目名称、单号、员工)定位器 + const $project_b = cede1.filter({ hasText: projectB }).filter({ hasText: randomNumA }); + // 数量、金额、现金业绩 + await expect.soft($project_b.locator('.m-table-cell').nth(6)).toContainText('1'); + await expect.soft($project_b.locator('.m-table-cell').nth(7)).toContainText(`${projectPriceB}`); + await expect($project_b.locator('.m-table-cell').nth(8)).toContainText(`${projectPriceB * 0.8}`); + + // 项目C(项目名称、单号、员工)定位器 + const $project_c = cede1.filter({ hasText: projectC }).filter({ hasText: randomNumA }); + // 数量、金额、现金业绩 + await expect.soft($project_c.locator('.m-table-cell').nth(6)).toContainText('1'); + await expect.soft($project_c.locator('.m-table-cell').nth(7)).toContainText(`${projectPriceC}`); + await expect($project_c.locator('.m-table-cell').nth(8)).toContainText(`${projectPriceC * 0.8}`); + // 关闭窗口 + await page.locator('.popup_content .close_icon').click(); + + // 打开项目消耗记录窗口 + await page.locator('.m-table-footer').nth(1).locator('.handleNumber').first().click(); + await page.locator('.popup_content .title', { hasText: '项目消耗记录' }).waitFor(); + // 项目A(项目名称、单号)定位器 + const $project_a_consume = cede1.filter({ hasText: projectA }).filter({ hasText: randomNumA }); + // 数量、消耗业绩 + await expect.soft($project_a_consume.locator('.m-table-cell').nth(6)).toContainText('1'); + await expect($project_a_consume.locator('.m-table-cell').nth(8)).toContainText(`${projectPriceA}`); + // 关闭窗口 + await page.locator('.popup_content .close_icon').click(); + + // 打开套餐包销售记录窗口 + await page.locator('.m-table-footer').nth(2).locator('.handleNumber').first().click(); + await page.locator('.popup_content .title', { hasText: '套餐包销售记录' }).waitFor(); + const $project_c_setmeal = cede1.filter({ hasText: SetMeal }).filter({ hasText: randomNumA }); + // 数量、金额、现金业绩 + await expect.soft($project_c_setmeal.locator('.m-table-cell').nth(4)).toContainText('1'); + await expect.soft($project_c_setmeal.locator('.m-table-cell').nth(5)).toContainText(`${SetMealPriceTC}`); + await expect($project_c_setmeal.locator('.m-table-cell').nth(6)).toContainText(`${SetMealPriceTC * 0.8}`); + // 关闭窗口 + await page.locator('.popup_content .close_icon').click(); + + // 打开卖品销售记录窗口 + await page.locator('.m-table-footer').nth(3).locator('.handleNumber').first().click(); + await page.locator('.popup_content .title', { hasText: '卖品销售记录' }).waitFor(); + const $project_d_product = cede1.filter({ hasText: Product }).filter({ hasText: randomNumA }); + // 数量、金额、现金业绩 + await expect.soft($project_d_product.locator('.m-table-cell').nth(5)).toContainText('1'); + await expect.soft($project_d_product.locator('.m-table-cell').nth(6)).toContainText(`${ProductPrice}`); + await expect($project_d_product.locator('.m-table-cell').nth(7)).toContainText(`${ProductPrice * 0.8}`); + // 关闭窗口 + await page.locator('.popup_content .close_icon').click(); + + // 打开会员卡销售记录窗口 + await page.locator('.m-table-footer').nth(4).locator('.handleNumber').first().click(); + await page.locator('.popup_content .title', { hasText: '会员卡销售记录' }).waitFor(); + const $project_e_card = cede1.filter({ hasText: cardname }).filter({ hasText: usernameA }); + // 数量、金额、现金业绩 + await expect.soft($project_e_card.locator('.m-table-cell').nth(4)).toContainText(`${cardPrice}`); + await expect($project_e_card.locator('.m-table-cell').nth(5)).toContainText(`${cardPrice * 0.8}`); + }); + }); + + test('自定义表格', async ({ page, homeNavigation, reportPage, performanceDetailReportPage }) => { + await test.step('打开业绩明细表保存报表数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('业绩明细表'); + await performanceDetailReportPage.updateReportDataIndex(); + await performanceDetailReportPage.updateReportDataForTableTotal(); + await reportPage.closeRecommendReportPage(); + }); + + const randomReportName = '自定义业绩明细表' + faker.string.alpha(3); + await test.step('新建一个业绩明细表,保存报表数据', async () => { + await reportPage.openCustomReportPage('业绩明细表'); + await page.getByRole('button', { name: '保存并查询' }).click(); + await page.getByPlaceholder('请输入最多15个字').fill(randomReportName); + await Promise.all([ + await page.getByRole('button', { name: /保\s存/ }).click(), + await waitSpecifyApiLoad(page, ['/api/report']), + ]); + await expect(page.getByRole('button', { name: /保\s存/ })).not.toBeVisible(); + await page.getByText('自定义报表').waitFor(); + await performanceDetailReportPage.updateReportDataForTableTotal(); + }); + + await test.step('对比报表数据', async () => { + const reportData = performanceDetailReportPage.reportData; + reportPage.toBeReportDataAsExpected('项目销售', '数量', reportData, 0); + reportPage.toBeReportDataAsExpected('项目销售', '金额', reportData, 0); + reportPage.toBeReportDataAsExpected('项目销售', '现金业绩', reportData, 0); + await reportPage.closeCustomReportPage(); + }); + + await test.step('编辑自定义报表', async () => { + await page + .locator('.m-report_custorm .item') + .filter({ has: page.getByText(randomReportName) }) + .locator('.arrow') + .click(); // 点击箭头 + await page.getByRole('menuitem', { name: '编辑' }).click(); // 点击删除 + await page.getByLabel('项目销售', { exact: true }).uncheck(); + await page.getByRole('button', { name: '保存并查询' }).click(); + + await expect(page.locator('.m-table__header-wrapper tr').first().locator('th').nth(1)).not.toContainText( + '项目销售', + ); + + await reportPage.closeCustomReportPage(); + }); + + await reportPage.deleteCustomReportPage(randomReportName); + }); +}); + +test.describe('项目销耗存表', () => { + test('数据校验', async ({ page, createCustomers, homeNavigation, reportPage, customerPage, numberInput }) => { + /** @type {Customer[]} */ + let customers = []; + await test.step('创建两个顾客', async () => { + customers = await createCustomers(2); + }); + if (!customers || customers.length < 2) { + throw new Error('创建顾客失败'); + } + const ca = customers[0]; + const cb = customers[1]; + // 获取姓名、手机号、档案号 + const usernameA = ca.username; + const phoneA = ca.phone; + const usernameB = cb.username; + const phoneB = cb.phone; + + // 项目A + const projectA = ProjectName.Projects.Projects_8; + // 项目B + const projectB = ProjectName.Projects.Projects_12; + // 项目C + const projectC = ProjectName.Projects.Projects_11; + + const $tr = page.locator('.m-table__body-wrapper').locator('.m-table__body tbody tr'); + const $project_a = $tr.filter({ hasText: projectA.name }); + const $project_b = $tr.filter({ hasText: projectB.name }); + const $project_c = $tr.filter({ hasText: projectC.name }); + + // 定义初始数据 + let SalesAmountVerify; + let SalesQuantityVerify; + let BuyersNumVerify; + let ConsumedAmountVerify; + let ConsumedQuantityVerify; + let ConsumedNumVerify; + let AdjustmentAmountBVerify; + let SwapoutQuantityVerify; + let AdjustmentAmountCVerify; + let SwapinQuantityVerify; + let TransfertoAmountVerify; + let TransfertoQuantityVerify; + let rolloutAmountVerify; + let rolloutQuantityVerify; + + await test.step('进入项目销耗存表,获取初始数据', async () => { + await homeNavigation.gotoModule('报表'); + await page.getByText('推荐报表').waitFor(); + await reportPage.gotoSubPage('项目销耗存表'); + await page.locator('.m-table__body-wrapper').waitFor(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + // 获取项目A的初始数据,不存在则为0 + try { + // 判断A项目是否存在 + await expect($project_a).toBeVisible({ timeout: 10_000 }); + // A初始销售金额 + await $project_a + .locator('.m-table-cell') + .nth(4) + .innerText() + .then(text => { + if (text === '--') { + SalesAmountVerify = 0; + } else { + SalesAmountVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // A初始销售数量 + await $project_a + .locator('.m-table-cell') + .nth(5) + .innerText() + .then(text => { + if (text === '--') { + SalesQuantityVerify = 0; + } else { + SalesQuantityVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // A初始购买人数 + await $project_a + .locator('.m-table-cell') + .nth(7) + .innerText() + .then(text => { + if (text === '--') { + BuyersNumVerify = 0; + } else { + BuyersNumVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // A初始消耗金额 + await $project_a + .locator('.m-table-cell') + .nth(10) + .innerText() + .then(text => { + if (text === '--') { + ConsumedAmountVerify = 0; + } else { + ConsumedAmountVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // A初始消耗数量 + await $project_a + .locator('.m-table-cell') + .nth(11) + .innerText() + .then(text => { + if (text === '--') { + ConsumedQuantityVerify = 0; + } else { + ConsumedQuantityVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // A初始消耗人数 + await $project_a + .locator('.m-table-cell') + .nth(13) + .innerText() + .then(text => { + if (text === '--') { + ConsumedNumVerify = 0; + } else { + ConsumedNumVerify = Number(KeepOnlyNumbers(text)); + } + }); + } catch (error) { + SalesAmountVerify = 0; + SalesQuantityVerify = 0; + BuyersNumVerify = 0; + ConsumedAmountVerify = 0; + ConsumedQuantityVerify = 0; + ConsumedNumVerify = 0; + console.log(`${projectA.name}不存在`); + } + + // 获取项目B的初始数据,不存在则为0 + try { + // 判断B项目是否存在 + await expect($project_b).toBeVisible({ timeout: 10_000 }); + // B初始调整金额 + await $project_b + .locator('.m-table-cell') + .nth(15) + .innerText() + .then(text => { + if (text === '--') { + AdjustmentAmountBVerify = 0; + } else { + AdjustmentAmountBVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // B初始换出数量 + await $project_b + .locator('.m-table-cell') + .nth(16) + .innerText() + .then(text => { + if (text === '--') { + SwapoutQuantityVerify = 0; + } else { + SwapoutQuantityVerify = Number(KeepOnlyNumbers(text)); + } + }); + } catch (error) { + AdjustmentAmountBVerify = 0; + SwapoutQuantityVerify = 0; + AdjustmentAmountCVerify = 0; + console.log(`${projectB.name}不存在`); + } + + // 获取项目C的初始数据,不存在则为0 + try { + // 判断C项目是否存在 + await expect($project_c).toBeVisible({ timeout: 10_000 }); + // C初始调整金额 + await $project_c + .locator('.m-table-cell') + .nth(15) + .innerText() + .then(text => { + if (text === '--') { + AdjustmentAmountCVerify = 0; + } else { + AdjustmentAmountCVerify = Number(KeepOnlyNumbers(text)); + } + }); + // C初始换入数量 + await $project_c + .locator('.m-table-cell') + .nth(17) + .innerText() + .then(text => { + if (text === '--') { + SwapinQuantityVerify = 0; + } else { + SwapinQuantityVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // C初始转入金额 + await $project_c + .locator('.m-table-cell') + .nth(20) + .innerText() + .then(text => { + if (text === '--') { + TransfertoAmountVerify = 0; + } else { + TransfertoAmountVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // C初始转入数量 + await $project_c + .locator('.m-table-cell') + .nth(22) + .innerText() + .then(text => { + if (text === '--') { + TransfertoQuantityVerify = 0; + } else { + TransfertoQuantityVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // C初始转出金额 + await $project_c + .locator('.m-table-cell') + .nth(23) + .innerText() + .then(text => { + if (text === '--') { + rolloutAmountVerify = 0; + } else { + rolloutAmountVerify = Number(KeepOnlyNumbers(text)); + } + }); + + // C初始转出数量 + await $project_c + .locator('.m-table-cell') + .nth(25) + .innerText() + .then(text => { + if (text === '--') { + rolloutQuantityVerify = 0; + } else { + rolloutQuantityVerify = Number(KeepOnlyNumbers(text)); + } + }); + } catch (error) { + AdjustmentAmountCVerify = 0; + SwapinQuantityVerify = 0; + TransfertoAmountVerify = 0; + TransfertoQuantityVerify = 0; + rolloutAmountVerify = 0; + rolloutQuantityVerify = 0; + console.log(`${projectC.name}不存在`); + } + }); + + await test.step('顾客A购买A项目并消耗掉', async () => { + const employee1 = Employees.FirstShop.Employee_6.name; + + // 点击顾客 + await page.reload(); + // 点击顾客 + await homeNavigation.gotoModule('顾客'); + // 输入信息 + await customerPage.searchCustomer(phoneA); + await customerPage.selectSearchCustomer(usernameA); + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: usernameA }).last().click(); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 购买项目A + await page.getByText(projectA.num).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + // 选择员工A + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 点击体验换成普通 + await page.locator('.type_btn').first().click(); + await page.locator('.type_item', { hasText: '普通' }).click(); + // 消耗该项目1次 + await page.locator('.commodity_item').last().click(); + // 点击添加员工 + await page.locator('.use_item .staff_btn').click(); + // 选择员工A + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结 算$/ }) + .click(); + // 结算,选择现金支付 + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + await test.step('顾客B购买项目B', async () => { + const employee2 = Employees.FirstShop.Employee_5.name; + // 点击顾客 + await page.reload(); + // 点击顾客 + await homeNavigation.gotoModule('顾客'); + // 输入信息 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(phoneB); + await page.getByText('搜索', { exact: true }).click(); + await page.getByText(usernameB).click(); + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: usernameB }).last().click(); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 购买项目B + await page.getByText(projectB.num).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + // 选择员工B + await page.locator('.hand_txt .name_txt').getByText(employee2).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 点击体验换成普通 + await page.locator('.type_btn').first().click(); + await page.locator('.type_item', { hasText: '普通' }).click(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结 算$/ }) + .click(); + // 结算,选择现金支付 + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + // 换入金额 + const exchangeAmount = 16900; + await test.step('顾客B将项目B置换为项目C', async () => { + await page.reload(); + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(phoneB); + await customerPage.selectSearchCustomer(usernameB); + await page.locator('.user_info_head .user_name', { hasText: usernameB }).last().click(); + + await page.locator('.treat_card').first().waitFor(); + // 点击项目右下角更多三点 + await page.locator('.treat_card .more').first().click(); + await page.locator('.ant-dropdown-menu-item', { hasText: '换项目' }).click(); + // 点击更换项目 + await page.locator('.add_btns .add_btn').click(); + // 输入项目C的编号 + await page.locator('.comSelect_content_body .ant-input').fill(projectC.num); + // 搜索 + await page.locator('.comSelect_content_body .ant-btn', { hasText: /^搜\s索$/ }).click(); + await expect(async () => { + // 选择项目 + await page.locator('.comSelect_content_body .ant-checkbox').click(); + await page.locator('.menu-item-dot').first().waitFor({ timeout: 2000 }); + await page.getByRole('button', { name: '确定选择' }).last().click(); + const verify = page.locator('.comSelect_title', { hasText: '选择项目' }).last(); + await expect(verify).not.toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 60000 }); + // 输入次数 + await page.locator('.num_input').first().click(); + await numberInput.setValue(1); + await numberInput.confirmValue(); + await page.locator('.num_input').nth(1).click(); + // 输入金额 + await numberInput.setValue(exchangeAmount); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 结算,选择现金支付 + await page.getByText('现金', { exact: true }).click(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + }); + + await test.step('进入项目消耗存,校验数据变化', async () => { + // 进入报表 + await page.reload(); + // 点击顾客 + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('项目销耗存表'); + await page.locator('.m-table__body-wrapper').waitFor(); + await expect(page.locator('.m-table__icon__warp')).toBeHidden(); + + // 项目A价格 + const projectAmountA = Number(projectA.Price); + // 项目B价格 + const projectAmountB = Number(projectB.Price); + + // 判断A项目是否存在 + await expect($project_a).toBeVisible(); + // A销售金额 + await expect + .soft($project_a.locator('.m-table-cell').nth(4)) + .toContainText(`${SalesAmountVerify + projectAmountA}`); + // A销售数量 + await expect.soft($project_a.locator('.m-table-cell').nth(5)).toContainText(`${SalesQuantityVerify + 1}`); + // A购买人数 + await expect.soft($project_a.locator('.m-table-cell').nth(7)).toContainText(`${BuyersNumVerify + 1}`); + // A消耗金额 + await expect + .soft($project_a.locator('.m-table-cell').nth(10)) + .toContainText(`${ConsumedAmountVerify + projectAmountA}`); + // A消耗数量 + await expect + .soft($project_a.locator('.m-table-cell').nth(11)) + .toContainText(`${ConsumedQuantityVerify + 1}`); + // A消耗人数 + await expect($project_a.locator('.m-table-cell').nth(13)).toContainText(`${ConsumedNumVerify + 1}`); + + // 判断B项目是否存在 + await expect($project_b).toBeVisible(); + // B调整金额 + await expect + .soft($project_b.locator('.m-table-cell').nth(15)) + .toContainText(`${AdjustmentAmountBVerify + projectAmountB}`); + // B换出数量 + await expect($project_b.locator('.m-table-cell').nth(16)).toContainText(`${SwapoutQuantityVerify + 1}`); + + // 判断C项目是否存在 + await expect($project_c).toBeVisible(); + // C调整金额 + await expect + .soft($project_c.locator('.m-table-cell').nth(15)) + .toContainText(`${AdjustmentAmountCVerify + exchangeAmount}`); + // C换入数量 + await expect($project_c.locator('.m-table-cell').nth(17)).toContainText(`${SwapinQuantityVerify + 1}`); + }); + }); + + test('自定义报表', async ({ page, homeNavigation, reportPage, itemSalesConsumptionAccessReportPage }) => { + const item = { name: '', reportData: [] }; + await test.step('打开项目销耗存表保存报表数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('项目销耗存表'); + + await itemSalesConsumptionAccessReportPage.updateReportDataIndex(); + item.name = (await page.locator('.m-table-fixed-body tr').first().locator('td').nth(0).innerText()).trim(); + await itemSalesConsumptionAccessReportPage.getSpecifyItemReportData(item.name, item.reportData); + await reportPage.closeRecommendReportPage(); + }); + + const randomReportName = '自定义项目销耗存表' + faker.string.alpha(3); + await test.step('新建一个项目销耗存表,保存报表数据', async () => { + await reportPage.openCustomReportPage('项目销耗存表'); + await page.getByRole('button', { name: '保存并查询' }).click(); + await page.getByPlaceholder('请输入最多15个字').fill(randomReportName); + await Promise.all([ + await page.getByRole('button', { name: /保\s存/ }).click(), + await waitSpecifyApiLoad(page, ['/api/report']), + ]); + await expect(page.getByRole('button', { name: /保\s存/ })).not.toBeVisible(); + await expect(page.getByText('自定义报表')).toBeVisible(); + }); + + await test.step('对比报表数据', async () => { + await itemSalesConsumptionAccessReportPage.getSpecifyItemReportData(item.name, item.reportData); + const reportData = item.reportData; + reportPage.toBeReportDataAsExpected('期初', '期初金额', reportData, 0); + reportPage.toBeReportDataAsExpected('期初', '期初数量', reportData, 0); + reportPage.toBeReportDataAsExpected('期初', '期初赠送数量', reportData, 0); + reportPage.toBeReportDataAsExpected('销售', '销售金额', reportData, 0); + reportPage.toBeReportDataAsExpected('销售', '销售数量', reportData, 0); + reportPage.toBeReportDataAsExpected('消耗', '消耗金额', reportData, 0); + await reportPage.closeCustomReportPage(); + }); + + await test.step('编辑自定义报表', async () => { + await page + .locator('.m-report_custorm .item') + .filter({ has: page.getByText(randomReportName) }) + .locator('.arrow') + .click(); // 点击箭头 + await page.getByRole('menuitem', { name: '编辑' }).click(); // 点击删除 + await page.getByLabel('期初', { exact: true }).uncheck(); + await page.getByRole('button', { name: '保存并查询' }).click(); + + await expect(page.locator('.m-table__header-wrapper tr').first().locator('th').nth(1)).not.toContainText( + '期初', + ); + + await reportPage.closeCustomReportPage(); + }); + + await reportPage.deleteCustomReportPage(randomReportName); + }); +}); + +test.describe('销售消耗汇总表', () => { + test('数据校验', async ({ page, homeNavigation, reportPage, createCustomer }) => { + const ca = createCustomer; + // 获取姓名、手机号、档案号 + const usernameA = ca.username; + const phoneA = ca.phone; + + let Cash; + let Expend; + await test.step('进入销售消耗汇总表,获取初始数据', async () => { + // 点击报表 + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('销售消耗汇总表'); + + // 获取初始现金业绩 + Cash = await page.locator('.m-table__body-wrapper .main-table-body_tr .m-table-cell').nth(1).innerText(); + // 获取初始消耗业绩 + Expend = await page.locator('.m-table__body-wrapper .main-table-body_tr .m-table-cell').nth(3).innerText(); + }); + + await test.step('购买项目', async () => { + await page.reload(); + await homeNavigation.gotoModule('顾客'); + // 输入信息 + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(phoneA); + await page.getByText('搜索', { exact: true }).click(); + await page.getByText(usernameA).click(); + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: usernameA }).last().click(); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 购买项目A + await page.getByText(ProjectName.Projects.Projects_4.num).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + // 选择员工A + const employee1 = Employees.FirstShop.Employee_6.name; + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 消耗该项目1次 + await page.locator('.commodity_item').last().click(); + // await page.locator('.treat_card_content').last().click();(消耗已经有的项目) + // 点击添加员工 + await page.locator('.use_item .staff_btn').click(); + // 选择员工A + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结 算$/ }) + .click(); + // 点击现金 + await page.getByText('现金').click(); + // 取消推送消息提醒 + await page.getByLabel('推送消费提醒').uncheck(); + // 取消结算签字 + await page.getByLabel('结算签字').uncheck(); + // 点击结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + }); + + await test.step('进入销售消耗汇总表,校验数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('销售消耗汇总表'); + + // 获取现金业绩 + const CashPerformance = await page + .locator('.m-table__body-wrapper .main-table-body_tr .m-table-cell') + .nth(1) + .innerText(); + // 获取消耗业绩 + const ExpendPerformance = await page + .locator('.m-table__body-wrapper .main-table-body_tr .m-table-cell') + .nth(3) + .innerText(); + + // 获取项目价格 + const ProjectCash = Number(Number(ProjectName.Projects.Projects_4.Price) * 0.8) + Number(Cash) + ''; + const ProjectPrice = Number(ProjectName.Projects.Projects_4.Price) + Number(Expend) + ''; + + // 对比现金业绩 + expect(CashPerformance).toBe(ProjectCash); + // 对比消耗业绩 + expect(ExpendPerformance).toBe(ProjectPrice); + }); + }); + + test('自定义报表', async ({ page, homeNavigation, reportPage, salesCostSummaryReportPage }) => { + await test.step('打开销售消耗汇总表保存报表数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('销售消耗汇总表'); + + await salesCostSummaryReportPage.updateReportDataIndex(); + await salesCostSummaryReportPage.updateReportData(1); + await reportPage.closeRecommendReportPage(); + }); + + const randomReportName = '自定义销售消耗汇总表' + faker.string.alpha(3); + await test.step('新建一个销售消耗汇总表,保存报表数据', async () => { + await reportPage.openCustomReportPage('销售消耗汇总表'); + await page.getByRole('button', { name: '保存并查询' }).click(); + await page.getByPlaceholder('请输入最多15个字').fill(randomReportName); + await Promise.all([ + await page.getByRole('button', { name: /保\s存/ }).click(), + await waitSpecifyApiLoad(page, ['/api/report']), + ]); + await expect(page.getByRole('button', { name: /保\s存/ })).not.toBeVisible(); + await expect(page.getByText('自定义报表')).toBeVisible(); + await salesCostSummaryReportPage.updateReportData(1); + }); + + await test.step('对比报表数据', async () => { + const reportData = salesCostSummaryReportPage.reportData; + reportPage.toBeReportDataAsExpected('护理', '现金业绩', reportData, 0); + reportPage.toBeReportDataAsExpected('面部', '现金业绩', reportData, 0); + reportPage.toBeReportDataAsExpected('身体', '现金业绩', reportData, 0); + reportPage.toBeReportDataAsExpected('组合项目', '现金业绩', reportData, 0); + await reportPage.closeCustomReportPage(); + }); + + await test.step('编辑自定义报表', async () => { + await page + .locator('.m-report_custorm .item') + .filter({ has: page.getByText(randomReportName) }) + .locator('.arrow') + .click(); // 点击箭头 + await page.getByRole('menuitem', { name: '编辑' }).click(); // 点击删除 + await page.getByLabel('护理', { exact: true }).check(); + await page.getByLabel('护理', { exact: true }).uncheck(); + await page.getByRole('button', { name: '保存并查询' }).click(); + + await expect(page.locator('.m-table__header-wrapper tr').first().locator('th').nth(1)).not.toContainText( + '护理', + ); + + await reportPage.closeCustomReportPage(); + }); + + await reportPage.deleteCustomReportPage(randomReportName); + }); +}); + +test.describe('业绩汇总表', () => { + test('数据校验', async ({ + page, + homeNavigation, + createCustomer, + reportPage, + performanceSummaryReportPage, + customerPage, + numberInput, + }) => { + // 项目 + const project = { no: '100019', name: '青春焕活套', price: 880, count: 1 }; + // 卖品 + const goods = { no: 'aa100045', name: '马郁兰精油', price: 800 }; + // 套餐 + const setMeal = { + name: '套餐包TC(BB004)', + price: 900, + projects: [ + { no: '', name: '净颜水润美肌套', price: 200, number: 1 }, + { no: '', name: '雪肌晶纯护理', price: 300, number: 1 }, + ], + goods: [{ no: '', name: '青春护理套', price: 400, number: 1 }], + }; + // 卡金 + let cardGold = 5000; + // 门店现金支付业绩比例 + let storeSalesRatio = 0.8; + // 充值会员卡金额 + let rechargeAmount = 1000; + // 其他--支出金额 + let spendingAmount = 1000; + + const customer = createCustomer; + + await test.step('拿取业绩汇总表的各个列的索引,和初始数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('业绩汇总表'); + + await performanceSummaryReportPage.updateReportDataIndex(); + await performanceSummaryReportPage.updateReportDataFromTotal(); + await reportPage.closeRecommendReportPage(); + }); + + await test.step('开原价卡(5000元,赠金3000元),使用现金结算', async () => { + // 打开顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.locator('.m-table__fixed-left').getByText(customer.username).click(); + // 去开卡 + await page + .locator('.info_package') + .getByRole('button', { name: /开\s卡/ }) + .click(); + await page.locator('.openCard_box', { hasText: '会员卡购买' }).waitFor(); + await page.locator('.openCard_box .memberCard_box', { hasText: '原价卡' }).click(); + await page.getByRole('button', { name: '去结算' }).click(); + // 使用现金进行结算 + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await page.locator('.popup_content', { hasText: '会员协议签署确认' }).waitFor(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + + // 给会员卡充值1000 + await page + .getByRole('button', { name: /^充\s值$/ }) + .first() + .click(); + await page + .locator('.left .row', { hasText: /充值\s总额/ }) + .locator('.touchIcon') + .first() + .click(); + await numberInput.setValue(rechargeAmount); + await numberInput.confirmValue(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('购买套餐和项目进行混合支付结算', async () => { + // 去购买项目、套餐、卖品 + await page.locator('.right_util .goto_pay_cash', { hasText: '去开单' }).click(); + // 购买项目project并消耗,价格880 + await page + .locator('.project_list .number', { + hasText: project.no, + }) + .click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买套餐,价格1000 + await page.locator('.float_tab .item', { hasText: '套餐' }).click(); + await page.locator('.item', { hasText: setMeal.name }).click(); + + // 购买卖品,价格800 + await page.locator('.float_tab .item', { hasText: '卖品' }).click(); + const goodsLocator = page.locator('.project_list .number').filter({ hasText: goods.no }); + + // 获取获取滚动窗口高度,每次滚动一个卖品div高度,去判断需要的卖品存在 + await page.locator('.project_box .scroller-body').waitFor(); + const scrollHeight = await page.evaluate(() => { + const scrollContainer = document.querySelector('.project_box .scroller-body'); + return scrollContainer ? scrollContainer.scrollHeight : 0; + }); + const moveHeight = await page + .locator('.list_box .project_list') + .first() + .boundingBox() + .then(box => { + if (box) { + return box.height; + } else { + return 0; + } + }); + + let startScroll = 0; + let endScroll = scrollHeight; + let findStatus = false; + while (startScroll < endScroll) { + startScroll += moveHeight; + console.log(startScroll); + // 每次移动一个高度的卖品盒子 + await page.evaluate(moveHeight => { + const scrollContainer = document.querySelector('.project_box .scroller-body'); + if (scrollContainer) { + scrollContainer.scrollBy(0, moveHeight); + } + }, moveHeight); + // 发现goodsLocator就退出 + findStatus = await goodsLocator.isVisible(); + console.log(findStatus); + if (findStatus) { + break; + } + } + console.log(findStatus); + await goodsLocator.scrollIntoViewIfNeeded(); + await goodsLocator.click(); + + await page.locator('.pay_btn', { hasText: /结\s算/ }).click(); + + // 混合支付 + // 左右两侧的支付方式定位器 + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + const payListLocator = page.locator('.service_area .itemlist_radio_group .ant-radio-wrapper'); + await leftPaymentInfoItem.filter({ hasText: '混合支付' }).click(); + + // 指定项目,使用卡金支付全部金额880 + await rightPaymentInfoItem.filter({ hasText: '卡金' }).click(); + await payListLocator.filter({ hasText: project.name }).first().getByRole('radio').check(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.getByPlaceholder('请输入内容@.').fill(String(project.price)); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + + // 指定套餐里的一个项目,使用赠金支付全部金额,雪肌晶纯护理 300 + await rightPaymentInfoItem.filter({ hasText: '赠金' }).click(); + await payListLocator + .filter({ hasText: setMeal.projects[setMeal.projects.length - 1].name }) + .first() + .getByRole('radio') + .check(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page + .locator('.popup_content input') + .fill(String(setMeal.projects[setMeal.projects.length - 1].price)); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + + // 使用现金支付所有的金额 + await rightPaymentInfoItem.filter({ hasText: '现金' }).first().click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + + // 结算 + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await page.locator('.popup_content', { hasText: '会员协议签署确认' }).waitFor(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + await page.getByRole('button', { name: '不寄存' }).click(); + }); + + await test.step('进入其他,对一店开支1000元', async () => { + await homeNavigation.gotoModule('其他'); + await page.locator('#frame_detail .top_tab .tab_item', { hasText: '开支' }).click(); + await page.locator('.shopname').click(); + await page.locator('.shopSelect_shop_content').click(); + // 门店选择器,选择一店 + await page.locator('.list_box .label').nth(1).click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await page.getByRole('button', { name: '新增支出' }).click(); + const selectInput = page.locator('.container .ant-form .item'); + await selectInput.filter({ hasText: '开支类型' }).locator('div').nth(1).click(); + await page.getByLabel('顾客餐').click(); + await page + .locator('.popup_content') + .getByRole('button', { name: /^确\s认$/ }) + .click(); + await selectInput.filter({ hasText: '开支金额' }).locator('div').nth(1).click(); + // 支出金额1000 + await numberInput.setValue(spendingAmount); + await numberInput.confirmValue(); + + await Promise.all([ + await page.getByRole('button', { name: /^确\s认$/ }).click(), + await page.waitForResponse(async res => { + return res.url().includes('/expense') && res.request().method() === 'POST'; + }), + ]); + }); + + await test.step('进入业绩汇总表查询并且对比数据', async () => { + // 进入业绩汇总表 + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('业绩汇总表'); + + await page.getByRole('cell', { name: '营收明细' }).waitFor(); + + // 拿取业绩汇总表相关数据 + await performanceSummaryReportPage.updateReportDataFromTotal(); + const reportData = performanceSummaryReportPage.reportData; + + console.log(JSON.stringify(reportData)); + + reportPage.toBeReportDataAsExpected( + '营收明细', + '现金类总额', + reportData, + cardGold + + rechargeAmount + + setMeal.price - + setMeal.projects[setMeal.projects.length - 1].price + + goods.price, + ); + // 卡金 + 充值卡金 + 套餐现金支付 + 卖品现金支付 + reportPage.toBeReportDataAsExpected( + '营收明细', + '现金', + reportData, + cardGold + + rechargeAmount + + setMeal.price - + setMeal.projects[setMeal.projects.length - 1].price + + goods.price, + ); + // 项目一 + reportPage.toBeReportDataAsExpected('营收明细', '划卡', reportData, project.price); + // 套餐内的项目四 + reportPage.toBeReportDataAsExpected( + '营收明细', + '划赠金', + reportData, + setMeal.projects[setMeal.projects.length - 1].price, + ); + + // 2. 开支明细--支出总额 + 1000 + reportPage.toBeReportDataAsExpected('开支明细', '支出总额', reportData, spendingAmount); + + // 3. 现金业绩 + reportPage.toBeReportDataAsExpected( + '现金业绩', + '项目合计', + reportData, + (setMeal.price - setMeal.projects[setMeal.projects.length - 1].price - setMeal.goods[0].price) * + storeSalesRatio, + ); + reportPage.toBeReportDataAsExpected( + '现金业绩', + '卖品业绩', + reportData, + (setMeal.goods[0].price + goods.price) * storeSalesRatio, + ); + reportPage.toBeReportDataAsExpected( + '现金业绩', + '开充卡业绩', + reportData, + (cardGold + 1000) * storeSalesRatio, + ); + reportPage.toBeReportDataAsExpected('现金业绩', '开卡业绩', reportData, cardGold * storeSalesRatio); + reportPage.toBeReportDataAsExpected('现金业绩', '充值业绩', reportData, 1000 * storeSalesRatio); + // (卡金 + 充值卡金 + 套餐现金支付 + 卖品现金支付) * 门店现金支付业绩比例 + reportPage.toBeReportDataAsExpected( + '现金业绩', + '总现金业绩', + reportData, + (cardGold + + rechargeAmount + + setMeal.price - + setMeal.projects[setMeal.projects.length - 1].price + + goods.price) * + storeSalesRatio, + ); + + // 4. 划卡业绩--指定项目880 + reportPage.toBeReportDataAsExpected('划卡业绩', '项目合计', reportData, project.price); + + // 5. 消耗业绩 + reportPage.toBeReportDataAsExpected('消耗业绩', '总消耗业绩', reportData, project.price); + reportPage.toBeReportDataAsExpected('消耗业绩', '项目总数', reportData, project.count); + + // 6. 客流 + reportPage.toBeReportDataAsExpected('客流', '客数', reportData, 1); + reportPage.toBeReportDataAsExpected('客流', '客次', reportData, 1); + + // 7. 套餐销售 + reportPage.toBeReportDataAsExpected('套餐销售', '总数量', reportData, 1); + reportPage.toBeReportDataAsExpected('套餐销售', '普通数量', reportData, 1); + reportPage.toBeReportDataAsExpected('套餐销售', '总金额', reportData, setMeal.price); + // 现金支付700 * 0.8 + 赠金支付300 + reportPage.toBeReportDataAsExpected( + '套餐销售', + '总业绩', + reportData, + (setMeal.price - setMeal.projects[setMeal.projects.length - 1].price) * storeSalesRatio + + setMeal.projects[setMeal.projects.length - 1].price, + ); + + // 8. 卖品销售 + reportPage.toBeReportDataAsExpected('卖品销售', '总数量', reportData, 2); + reportPage.toBeReportDataAsExpected('卖品销售', '普通数量', reportData, 2); + reportPage.toBeReportDataAsExpected('卖品销售', '总金额', reportData, goods.price + setMeal.goods[0].price); + reportPage.toBeReportDataAsExpected( + '卖品销售', + '总业绩', + reportData, + (goods.price + setMeal.goods[0].price) * storeSalesRatio, + ); + }); + }); + + test('自定义报表', async ({ page, homeNavigation, reportPage, performanceSummaryReportPage }) => { + await test.step('打开业绩汇总表保存报表数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('业绩汇总表'); + await performanceSummaryReportPage.updateReportDataIndex(); + await performanceSummaryReportPage.updateReportDataFromTotal(); + + await reportPage.closeRecommendReportPage(); + }); + + const randomReportName = '自定义业绩汇总表' + faker.string.alpha(3); + await test.step('新建一个业绩汇总表,保存报表数据', async () => { + await reportPage.openCustomReportPage('业绩汇总表'); + await page.getByRole('button', { name: '保存并查询' }).click(); + await page.getByPlaceholder('请输入最多15个字').fill(randomReportName); + await Promise.all([ + await page.getByRole('button', { name: /保\s存/ }).click(), + await waitSpecifyApiLoad(page, ['/api/report']), + ]); + await expect(page.getByRole('button', { name: /保\s存/ })).not.toBeVisible(); + await expect(page.getByText('自定义报表')).toBeVisible(); + await performanceSummaryReportPage.updateReportDataFromTotal(); + }); + + await test.step('对比报表数据', async () => { + const reportData = performanceSummaryReportPage.reportData; + reportPage.toBeReportDataAsExpected('营收明细', '现金', reportData, 0); + reportPage.toBeReportDataAsExpected('营收明细', '划卡', reportData, 0); + reportPage.toBeReportDataAsExpected('营收明细', '划赠金', reportData, 0); + reportPage.toBeReportDataAsExpected('开支明细', '支出总额', reportData, 0); + await reportPage.closeCustomReportPage(); + }); + + await test.step('编辑自定义报表', async () => { + await page + .locator('.m-report_custorm .item') + .filter({ has: page.getByText(randomReportName) }) + .locator('.arrow') + .click(); // 点击箭头 + await page.getByRole('menuitem', { name: '编辑' }).click(); // 点击删除 + await page.getByLabel('营收明细', { exact: true }).check(); + await page.getByLabel('营收明细', { exact: true }).uncheck(); + await page.getByRole('button', { name: '保存并查询' }).click(); + + await expect(page.locator('.m-table__header-wrapper tr').first().locator('th').nth(1)).not.toContainText( + '营收明细', + ); + + await reportPage.closeCustomReportPage(); + }); + + await reportPage.deleteCustomReportPage(randomReportName); + }); +}); + +test.describe('储值卡卡金变动表', () => { + test('数据校验', async ({ + page, + homeNavigation, + reportPage, + cardBalanceChangeReportPage, + createCustomer, + customerPage, + numberInput, + }) => { + const customer = createCustomer; + + const card_a = { + cardNo: '', + name: '原价卡', + cardGold: 5000, + cardBonus: 3000, + remark: '卡A', + reportData: [], + }; + + await test.step('进入储值卡卡金变动表,获取报表索引、卡A数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('储值卡卡金变动表'); + // 更新报表索引 + await cardBalanceChangeReportPage.updateReportDataIndex(); + // 获取卡A的报表数据 + await cardBalanceChangeReportPage.getSpecifyCardReportData(card_a.name, card_a.reportData); + // 关闭推荐报表 + await reportPage.closeRecommendReportPage(); + }); + + // 开卡窗口的各个开的定位器 + const openCardBox = page.locator('.openCard_box .memberCard_box'); + + await test.step('给顾客A进行开卡', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await page.locator('.m-table__fixed-left').getByText(customer.username).click(); + await page.getByRole('button', { name: /开\s卡/ }).click(); + await page.locator('.openCard_box', { hasText: '会员卡购买' }).waitFor(); + + // 选择卡A,备注以后,进行结算 + await openCardBox + .filter({ + has: page.locator('.card_name', { hasText: new RegExp(`^${card_a.name}$`) }), + }) + .click(); + await page.locator('.remark', { hasText: '可输入卡备注' }).click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(card_a.remark); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.getByRole('button', { name: '去结算' }).click(); + + // 使用现金进行结算 + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await page.locator('.popup_content', { hasText: '会员协议签署确认' }).waitFor(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + }); + + await test.step('卡A,充1000,赠200,修改卡金+1000', async () => { + // 充值金额 + const rechargeAmount = 1000; + // 赠送金额 + const bonusAmount = 200; + // 修改卡金金额 + const modifyCardGold = 1000; + + // 给会员卡充值1000,赠送200 + await page + .getByRole('button', { name: /^充\s值$/ }) + .first() + .click(); + await page + .locator('.left .row', { hasText: /充值\s总额/ }) + .locator('.touchIcon') + .first() + .click(); + await numberInput.setValue(rechargeAmount); + await numberInput.confirmValue(); + await page.locator('.left .row', { hasText: '赠送金' }).locator('.anticon').last().click(); + await numberInput.setValue(bonusAmount); + await numberInput.confirmValue(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + + await page.locator('.membercard_box', { hasText: '卡A' }).locator('.card_control_btn > .more').click(); + await page.getByRole('menuitem', { name: '修改卡金' }).click(); + await numberInput.setValue(card_a.cardGold + rechargeAmount + modifyCardGold); + await numberInput.confirmValue(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('修改卡金+1000'); + await page.getByRole('button', { name: /确\s认/ }).click(); + await expect( + page.getByText(`¥${card_a.cardGold + rechargeAmount + modifyCardGold}`, { + exact: true, + }), + ).toBeVisible(); + await page.locator('.close_icons').click(); + }); + + await test.step('进入储值卡卡金变动表,进行比较卡金变动', async () => { + // 进入储值卡卡金变动表 + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('储值卡卡金变动表'); + + // 获取卡A的报表数据 + await cardBalanceChangeReportPage.getSpecifyCardReportData(card_a.name, card_a.reportData); + // 查看会员卡A的变动 + reportPage.toBeReportDataAsExpected('开充卡', '开充卡金', card_a.reportData, card_a.cardGold + 1000); + reportPage.toBeReportDataAsExpected('开充卡', '开充赠金', card_a.reportData, card_a.cardBonus + 200); + + reportPage.toBeReportDataAsExpected('调整', '修改卡金', card_a.reportData, 1000); + reportPage.toBeReportDataAsExpected('结余', '结余卡金', card_a.reportData, card_a.cardGold + 2000); + + reportPage.toBeReportDataAsExpected('结余', '结余赠金', card_a.reportData, card_a.cardBonus + 200); + + reportPage.toBeReportDataAsExpected('变动', '卡金变动', card_a.reportData, card_a.cardGold + 2000); + + reportPage.toBeReportDataAsExpected('变动', '赠金变动', card_a.reportData, card_a.cardBonus + 200); + }); + }); + + test('自定义报表', async ({ page, homeNavigation, reportPage, cardBalanceChangeReportPage }) => { + const card = { + cardNo: '', + name: '原价卡', + cardGold: 5000, + cardBonus: 3000, + remark: '卡A', + reportData: [], + }; + + await test.step('打开储值卡卡金变动表保存报表数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('储值卡卡金变动表'); + await cardBalanceChangeReportPage.updateReportDataIndex(); + await cardBalanceChangeReportPage.getSpecifyCardReportData(card.name, card.reportData); + await reportPage.closeRecommendReportPage(); + }); + + const randomReportName = '自定义储值卡卡金变动表' + faker.string.alpha(3); + await test.step('新建一个储值卡卡金变动表,保存报表数据', async () => { + await reportPage.openCustomReportPage('储值卡卡金变动表'); + await page.getByRole('button', { name: '保存并查询' }).click(); + await page.getByPlaceholder('请输入最多15个字').fill(randomReportName); + await Promise.all([ + await page.getByRole('button', { name: /保\s存/ }).click(), + await waitSpecifyApiLoad(page, ['/api/report']), + ]); + await expect(page.getByRole('button', { name: /保\s存/ })).not.toBeVisible(); + await expect(page.getByText('自定义报表')).toBeVisible(); + }); + + await test.step('对比报表数据', async () => { + await cardBalanceChangeReportPage.getSpecifyCardReportData(card.name, card.reportData); + const reportData = card.reportData; + reportPage.toBeReportDataAsExpected('期初', '期初卡金', reportData, 0); + reportPage.toBeReportDataAsExpected('期初', '期初赠金', reportData, 0); + reportPage.toBeReportDataAsExpected('消费', '消费卡金', reportData, 0); + reportPage.toBeReportDataAsExpected('消费', '消费赠金', reportData, 0); + reportPage.toBeReportDataAsExpected('开充卡', '开充卡金', reportData, 0); + reportPage.toBeReportDataAsExpected('开充卡', '开充赠金', reportData, 0); + await reportPage.closeCustomReportPage(); + }); + + await test.step('编辑自定义报表', async () => { + await page + .locator('.m-report_custorm .item') + .filter({ has: page.getByText(randomReportName) }) + .locator('.arrow') + .click(); // 点击箭头 + await page.getByRole('menuitem', { name: '编辑' }).click(); // 点击删除 + await page.getByLabel('期初', { exact: true }).check(); + await page.getByLabel('期初', { exact: true }).uncheck(); + await page.getByRole('button', { name: '保存并查询' }).click(); + + await expect(page.locator('.m-table__header-wrapper tr').first().locator('th').nth(1)).not.toContainText( + '期初', + ); + + await reportPage.closeCustomReportPage(); + }); + + await reportPage.deleteCustomReportPage(randomReportName); + }); +}); + +test.describe('开支汇总表', () => { + test('数据校验', async ({ page, homeNavigation, reportPage, spendingSummaryReportPage, numberInput }) => { + // 进入开支汇总表 + await test.step('进入开支汇总表,获取数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('开支汇总表'); + await spendingSummaryReportPage.updateReportDataIndex(); + await spendingSummaryReportPage.updateReportData(1); + await reportPage.closeRecommendReportPage(); + }); + + await test.step('新增门店一的开支', async () => { + await homeNavigation.gotoModule('其他'); + await page.locator('#frame_detail .top_tab .tab_item', { hasText: '开支' }).click(); + await page.locator('.shopname').click(); + await page.locator('.shopSelect_shop_content').click(); + // 门店选择器,选择一店 + await page.locator('.list_box .label').nth(1).click(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await page.getByRole('button', { name: '新增支出' }).click(); + const selectInput = page.locator('.container .ant-form .item'); + await selectInput.filter({ hasText: '开支类型' }).locator('div').nth(1).click(); + await page.getByLabel('顾客餐').click(); + await page + .locator('.popup_content') + .getByRole('button', { name: /^确\s认$/ }) + .click(); + await selectInput.filter({ hasText: '开支金额' }).locator('div').nth(1).click(); + // 支出金额1000 + await numberInput.setValue(1000); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + }); + + await test.step('进入开支汇总表,获取数据进行比较', async () => { + // 进入开支汇总表 + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('开支汇总表'); + await page.getByRole('cell', { name: '门店', exact: true }).waitFor(); + await spendingSummaryReportPage.updateReportData(1); + await reportPage.closeRecommendReportPage(); + + const reportData = spendingSummaryReportPage.reportData; + reportPage.toBeReportDataAsExpected('支出汇总', '累计支出', reportData, 1000); + reportPage.toBeReportDataAsExpected('支出汇总', '营业收入支出', reportData, 1000); + reportPage.toBeReportDataAsExpected('日常开支', '顾客餐', reportData, 1000); + }); + }); +}); + +test.describe('顾客消费分析表', () => { + test('数据校验', async ({ + page, + homeNavigation, + createCustomer, + reportPage, + customerPage, + customerConsumptionAnalysisReportPage, + }) => { + const customer = createCustomer; + + // 购买并消耗项目 + const project = { + no: '100018', + name: '苹果精萃护理', + shortName: '精萃护理', + price: 980, + }; + + await test.step('进入顾客消费分析表,获取数据', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('顾客消费分析表'); + await page.getByRole('cell', { name: '顾客' }).waitFor(); + + await page.getByRole('button').nth(2).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await Promise.all([ + page + .locator('.shopSelect_box') + .getByRole('button', { name: /查\s询/ }) + .click(), + page.waitForResponse('**/GKXFFX/**'), + ]); + + await customerConsumptionAnalysisReportPage.updateReportDataIndex(); + await customerConsumptionAnalysisReportPage.updateReportData(customer); + await reportPage.closeRecommendReportPage(); + }); + + await test.step('购买项目并消耗,获取数据进行比较', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + + // 购买项目并消耗,价格980 + await page + .locator('.project_list .number', { + hasText: project.no, + }) + .click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 结算 + await page.getByText(/结\s算/).click(); + + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.paytype .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + await expect(page.getByRole('button', { name: /开\s单/ })).toBeVisible(); + }); + + await test.step('进入顾客消费分析表,获取数据进行比较', async () => { + await homeNavigation.gotoModule('报表'); + await reportPage.gotoSubPage('顾客消费分析表'); + await page.getByRole('cell', { name: '顾客' }).waitFor(); + + await page.getByRole('button').nth(2).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await Promise.all([ + page + .locator('.shopSelect_box') + .getByRole('button', { name: /查\s询/ }) + .click(), + page.waitForResponse('**/GKXFFX/**'), + ]); + + await customerConsumptionAnalysisReportPage.updateReportData(customer); + + const reportData = customerConsumptionAnalysisReportPage.reportData; + + console.log(reportData); + + reportPage.toBeReportDataAsExpected('到店次数', '', reportData, 1); + reportPage.toBeReportDataAsExpected('消费次数', '', reportData, 1); + reportPage.toBeReportDataAsExpected('消费金额', '', reportData, project.price); + // reportPage.toBeReportDataAsExpected('护理(购买)', '现金', reportData, project.price); + // reportPage.toBeReportDataAsExpected('护理(购买)', '划卡金', reportData, 0); + }); + }); +}); diff --git a/tests/touch/boss_wastebook.spec.ts b/tests/touch/boss_wastebook.spec.ts new file mode 100644 index 0000000..43acf2e --- /dev/null +++ b/tests/touch/boss_wastebook.spec.ts @@ -0,0 +1,1244 @@ +// @ts-check +import { test, expect } from '@/fixtures/boss_common.js'; +import { faker } from '@faker-js/faker/locale/zh_CN'; +import { Customer } from '@/utils/customer'; +import { KeepOnlyNumbers } from '@/utils/utils.js'; +import fs from 'fs'; +import path from 'path'; +import { Employees, ProjectName } from '@/fixtures/userconfig.js'; +import { staffData } from '@/fixtures/staff.js'; + +test.describe('营业记录', () => { + test.beforeEach(async ({ page }) => { + await page.addLocatorHandler(page.getByRole('button', { name: '我知道了' }), async () => { + await page.getByRole('button', { name: '我知道了' }).click(); + await expect(page.getByRole('button', { name: '我知道了' })).not.toBeVisible(); + await page.reload(); + }); + + await page.addLocatorHandler(page.locator('.ant-notification', { hasText: '反结算成功' }), async () => { + await page.locator('.ant-notification', { hasText: '反结算成功' }).locator('a').click(); + await expect(page.locator('.ant-notification', { hasText: '反结算成功' })).not.toBeVisible(); + }); + }); + + test('根据条件搜索营业记录', async ({ page, homeNavigation, customerPage, createCustomCustomer }) => { + const customer = new Customer(2, 1); + const project = { num: '100012', name: '雪肌晶纯护理', Price: 300 }; + let billNo = ''; + + await test.step('创建顾客', async () => { + await createCustomCustomer(customer); + }); + + await test.step('开单结算拿取单号', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + await expect(page.locator('div').filter({ hasText: /^结\s算$/ })).toBeVisible(); + // 选择项目1 + await page.getByText(project.num).click(); + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + await page.locator('.paymentInfoItem').filter({ hasText: '现金' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /^结\s算$/ }).click(), + ]); + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + }); + expect(billNo).not.toBeNull(); + + const $bill = page.getByRole('cell', { name: billNo }).nth(1); + + await test.step('条件搜索', async () => { + await homeNavigation.gotoModule('流水'); + await expect(page.getByText('营业记录').first()).toBeVisible(); + await page.getByRole('combobox').first().click(); + await page.getByRole('option', { name: '购买项目/卖品' }).click(); + await page.getByRole('combobox').nth(1).click(); + await page.getByRole('option', { name: '已结算' }).click(); + await page.getByRole('combobox').nth(2).click(); + await page.getByRole('option', { name: '未对单' }).click(); + + await expect($bill).toBeVisible(); + }); + + await test.step('搜索水单号', async () => { + await page.getByRole('button').click(); + await page.getByPlaceholder('输入流水单号搜索').fill(billNo); + await page.locator('.search_btn > svg').click(); + + await expect($bill).toBeVisible(); + }); + + await test.step('进入水单详情和顾客详情', async () => { + await $bill.click(); + await expect(page.locator('li').getByText(billNo)).toBeVisible(); + await page + .locator('div') + .filter({ hasText: /^单据明细$/ }) + .locator('use') + .click(); + await page.locator('.main-table-body_tr').getByText(customer.username).first().click(); + await expect(page.getByRole('tabpanel').getByText(customer.username)).toBeVisible(); + }); + }); + + test.describe('单据明细', () => { + test.beforeAll(async () => { + const currentFileName = path.basename(__filename); + // 构造快照目录 + const snapshotDir = path.resolve('tests/imgs/__screenshots__/touch', currentFileName); + + // 如果目录不存在,则创建它 + if (!fs.existsSync(snapshotDir)) { + fs.mkdirSync(snapshotDir, { recursive: true }); + } + }); + + test('反结算', async ({ page, homeNavigation, customerPage, createCustomer, numberInput }) => { + // 定义随机单号 + let ReceiptNum = faker.helpers.fromRegExp(/1[3-9][0-9]{8}/); + const project_1 = ProjectName.Projects.Projects_2; // 项目 + const employee_1 = Employees.FirstShop.Employee_6; // 员工 + const project_2 = ProjectName.Projects.Projects_3; // 项目 + const employee_2 = Employees.FirstShop.Employee_4; // 员工 + // 创建顾客A随机姓名 手机 + const customer = createCustomer; + + await test.step('开单结算拿取单号', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 选择项目1 + await page.getByText(project_1.num).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + // 选择员工1 + await page.locator('.hand_txt .name_txt').getByText(employee_1.name).click(); + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + // 点击修改单号 + await page.locator('.input165').click(); + await page.getByPlaceholder('请输入内容').fill(ReceiptNum); + await page.locator('.tools_icon').last().click(); + // 点击现金 + await page.locator('.paymentInfoItem').filter({ hasText: '现金' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message')).toContainText('结算成功'); + }); + + await test.step('进入流水模块,进行反结算', async () => { + await homeNavigation.gotoModule('流水'); + // 点击指定单号 + await page + .locator('.m-table__fixed-left .m-table-cell .bill .m-dropdown-link ', { + hasText: ReceiptNum, + }) + .click(); + // 点击反结算 + await page.locator('.oversized', { hasText: '反结算' }).click(); + await page.locator('.custom_content').waitFor(); + // 删除单据内的项目 + await page.locator('.buy_item .buy_name .del_btn').last().click(); + // 选择项目C + await page.getByText(project_2.num).click(); + await page.locator('#buyList').getByText(project_2.name).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + await page.getByText(employee_2.name).first().click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 修改数量 + await page.locator('.edit_txt div:nth-child(2)').first().click(); + await numberInput.setValue(2); + await numberInput.confirmValue(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + // 使用银联支付 + await page.locator('.paymentInfoItem', { hasText: '银联' }).click(); + await page.getByLabel('结算签字').uncheck(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message', { hasText: '结算成功' })).toBeVisible(); + + await page.addLocatorHandler(page.getByRole('button', { name: '我知道了' }), async () => { + await page.getByRole('button', { name: '我知道了' }).click(); + await expect(page.getByRole('button', { name: '我知道了' })).not.toBeVisible(); + await page.reload(); + await page + .locator('.m-table__fixed-left .m-table-cell .bill .m-dropdown-link ', { + hasText: ReceiptNum, + }) + .click(); + }); + }); + + await test.step('进入单据明细,查看反结算内容', async () => { + await page.locator('.m-receiptDetail_tip', { hasText: '单据明细' }).waitFor(); + // 对比第一个项目 + await expect.soft(page.locator('.items_name .name_txt').first()).toContainText(project_2.name); + // 对比员工 + await expect + .soft( + page + .locator( + '.m-detailComponent_report .main-table-body_tr:nth-child(1) .billUser_item .userName', + ) + .first(), + ) + .toContainText(employee_2.name); + // 对比数量 + await expect.soft(page.locator('.m-detailComponent-cell .num').last()).toContainText('2'); + // 对比总金额 + const project1ResultCollect = Number(ProjectName.Projects.Projects_3.Price) * 2 + ''; + await expect(page.locator('.m-detailComponent_consume_title .amount')).toContainText( + project1ResultCollect, + ); + }); + }); + + test('撤单', async ({ page, homeNavigation, customerPage, createCustomer }) => { + const ReceiptNum = faker.helpers.fromRegExp(/1[3-9][0-9]{8}/); // 随机单号 + let OddNumber = page.locator('.m-table__fixed-left .m-table-cell .bill .m-dropdown-link ', { + hasText: ReceiptNum, + }); + const project = ProjectName.Projects.Projects_3; + + const customer = createCustomer; + + await test.step('开单获取单号', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 选择项目1 + await page.getByText(project.num).click(); + // 结算 + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + // 点击修改单号 + await page.locator('.input165').click(); + // 输入随机单号 + await page.getByPlaceholder('请输入内容').fill(ReceiptNum); + // 确认 + await page.locator('.tools_icon').last().click(); + await page.locator('.paymentInfoItem', { hasText: '现金' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.getByRole('button', { name: /^结\s算$/ }).click(); + await expect(page.locator('.ant-message')).toContainText('结算成功'); + }); + + await test.step('根据单号进行撤单,判断撤单成功', async () => { + // 点击流水 + await page.reload(); + await homeNavigation.gotoModule('流水'); + // 点击下拉框 + await page.locator('.ant-dropdown-link > .anticon > svg').first().click(); + // 选择营业记录 + await page.getByRole('menuitem', { name: '营业记录' }).click(); + // 点击指定单号 + await page + .locator('.m-table__fixed-left .m-table-cell .bill .m-dropdown-link ', { + hasText: ReceiptNum, + }) + .click(); + // 点击撤单 + await page.locator('.ant-btn', { hasText: '撤单' }).click(); + // 点击输入备注 + await page.locator('.label_text').last().click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill('撤单'); + await page.locator('.saveCheck').waitFor(); + await page.getByRole('button', { name: '确 认' }).click(); + + // 点击下拉框 + await page.locator('.ant-dropdown-link > .anticon > svg').first().click(); + // 选择营业记录 + await page.getByRole('menuitem', { name: '营业记录' }).click(); + // 点击撤单查看 + await page.locator('.search_select').first().click(); + await page.locator('.ant-select-dropdown-menu-item', { hasText: '已撤单' }).click(); + // 判断撤的单子是否还在 + await expect(OddNumber).toBeVisible(); + }); + }); + + test('补签', async ({ page, homeNavigation, customerPage, createCustomer }) => { + const customer = createCustomer; + const project = { num: '100012', name: '雪肌晶纯护理', Price: 300 }; + + let billNo = ''; + + await test.step('开单结算,不勾选签字,拿取单号', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 选择项目1 + await page.getByText(project.num).click(); + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + await page.locator('.paymentInfoItem').filter({ hasText: '现金' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /^结\s算$/ }).click(), + ]); + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + expect(billNo).not.toBeNull(); + }); + + const $bill = page.getByRole('cell', { name: billNo }).nth(1); + + await test.step('进入水单详情进行补签', async () => { + await homeNavigation.gotoModule('流水'); + await expect(page.getByText('营业记录').first()).toBeVisible(); + await $bill.click(); + await expect(page.locator('li').getByText(billNo)).toBeVisible(); + await page.getByText('补签').click(); + await expect(page.getByText('请签字确认消费')).toBeVisible(); + const $canvas = page.locator('canvas').first(); + const canvasBoundingBox = await $canvas.boundingBox(); + if (canvasBoundingBox) { + const { x, y, width, height } = canvasBoundingBox; + + // 模拟鼠标按下操作 + await page.mouse.move(x + width / 4, y + height / 2); + await page.mouse.down(); + + // 模拟签字路径(可以自定义路径) + await page.mouse.move(x + width / 2, y + height / 2); + await page.mouse.move(x + width / 1.5, y + height / 3); + await page.mouse.move(x + width / 1.2, y + height / 1.8); + + // 释放鼠标 + await page.mouse.up(); + } else { + throw new Error('无法找到签字区域'); + } + + await page.getByRole('button', { name: '签好了' }).click(); + await expect(page.locator('li').getByText(billNo)).toBeVisible(); + await page.waitForLoadState('load'); + + const $img = page.locator('.slider_list_item', { has: page.getByText('签名') }).locator('img'); + await expect($img).toHaveScreenshot({ + threshold: 0.25, // 相似度阈值(0.1 表示 10% 以内的差异可以接受) + animations: 'disabled', // 处理有动画的元素 + }); + }); + }); + + test('查看配方', async ({ page, homeNavigation, customerPage, createCustomer }) => { + const project = { num: '100012', name: '雪肌晶纯护理', Price: 300 }; + const customer = createCustomer; + + let billNo = ''; + let useProductName = ''; + await test.step('开单,购买项目并消耗,消耗时选择项目配方', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + // 选择项目1 + await page.getByText(project.num).click(); + await page.locator('.commodity_item').first().click(); + // 打开使用区-项目配方 + await page.locator('.staff_setting').first().click(); + await page.getByLabel('项目配方').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + // 选择项目配方 + await page.locator('.formula_noData').click(); + await expect(async () => { + await page.locator('.list_box').getByRole('checkbox').first().check(); + await expect(page.locator('.menu-item-dot').first()).toBeVisible(); + }).toPass({ timeout: 30_000 }); + useProductName = (await page.locator('.list_box .label').first().innerText()).trim(); + + await page.getByRole('button', { name: '确定选择' }).click(); + await page.locator('input[type="tel"]').fill('1'); + await page.getByRole('button', { name: /保\s存/ }).click(); + + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + await page.locator('.paymentInfoItem').filter({ hasText: '现金' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + // 结算 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /^结\s算$/ }).click(), + ]); + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + }); + expect(billNo).not.toBeNull(); + expect(useProductName).not.toBeNull(); + + await test.step('查看单据明细的配方消耗', async () => { + await homeNavigation.gotoModule('流水'); + await page.locator('.m-table-fixed-body').getByText(billNo).first().click(); + const $$listItem = page.locator('.slider_list_item'); + const $openConsumptionRecord = $$listItem + .filter({ + has: page.getByText('开用记录'), + }) + .getByText('查看详情'); + + await $openConsumptionRecord.click(); + const $popup = page.locator('.popup_content'); + await expect($popup.getByText(useProductName)).toBeVisible(); + const useProductNumber = ( + await $popup.locator('.main-table-body_tr').first().locator('td').nth(1).innerText() + ).trim(); + expect(useProductNumber).toBe('1'); + }); + }); + }); +}); + +test.describe('业绩流水', () => { + test('根据条件搜索业绩流水', async ({ + page, + customerPage, + homeNavigation, + createCustomer, + wasteBookBusinessRecordPage, + }) => { + const customer = createCustomer; + + let billNo = ''; + const firstProject = { num: '', name: '' }; + const firstGoods = { num: '', name: '' }; + await test.step('开单,购买项目、卖品并消耗,消耗购买项目', async () => { + // 进入顾客详情页面 + await homeNavigation.gotoModule('顾客'); + await customerPage.searchCustomer(customer.phone); + await customerPage.selectSearchCustomer(customer.username); + await customerPage.openCustomerDetail(customer.username, customer.phone); + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + await expect(page.locator('div').filter({ hasText: /^结\s算$/ })).toBeVisible(); + + // 选择项目1 + const $firstProject = page.locator('.list_box .project_list').first(); + await $firstProject.click(); + firstProject.num = await $firstProject.locator('.number').innerText(); + firstProject.name = await $firstProject.locator('.title').innerText(); + await page.locator('.commodity_item').first().click(); + + // 选择卖品 + await page.getByText('卖品', { exact: true }).click(); + await $firstProject.click(); + firstGoods.num = await $firstProject.locator('.number').innerText(); + firstGoods.name = await $firstProject.locator('.title').innerText(); + + await page + .locator('div') + .filter({ hasText: /^结\s算$/ }) + .click(); + await page.locator('.paymentInfoItem').filter({ hasText: '现金' }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && (await res.json()).code === 'SUCCESS'; + }), + page.getByRole('button', { name: /^结\s算$/ }).click(), + ]); + await page.getByRole('button', { name: '不寄存' }).click(); + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + }); + expect(billNo).not.toBeNull(); + expect(firstProject.num).not.toBe(''); + expect(firstProject.name).not.toBe(''); + expect(firstGoods.num).not.toBe(''); + expect(firstGoods.name).not.toBe(''); + + await test.step('根据筛选条件进行搜索水单', async () => { + await Promise.all([homeNavigation.gotoModule('流水'), page.waitForLoadState()]); + await wasteBookBusinessRecordPage.gotoSubPage('业绩流水'); + + // 流水单 + const $flowChart = page.getByRole('row', { name: billNo, exact: true }).first(); + const $projectViewSelect = page + .locator('.com_picker') + .filter({ hasText: '选择' }) + .locator('.menu-item-dot') + .first(); + const $goodsViewSelect = page + .locator('.com_picker') + .filter({ hasText: '选择卖品' }) + .locator('.menu-item-dot') + .first(); + + await page.getByTitle('全部水单类型').click(); + await page.getByRole('option', { name: '购买项目/卖品' }).click(); + // 选择项目 + await page.locator('span').filter({ hasText: '选择项目' }).getByRole('list').click(); + await page.getByRole('textbox', { name: '请输入首字母或关键字检索' }).fill(firstProject.num); + await page.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await page.getByLabel(firstProject.name).uncheck(); + await page.getByLabel(firstProject.name).check(); + await expect($projectViewSelect).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 30_000 }); + await page.getByRole('button', { name: '确定选择' }).click(); + + // 选择卖品 + await page.getByTitle('购买项目/卖品').click(); + await page.getByRole('option', { name: '全部水单类型' }).click(); + await page.getByTitle('全部水单类型').click(); + await page.getByRole('option', { name: '购买项目/卖品' }).click(); + await page.locator('span').filter({ hasText: '选择卖品' }).getByRole('list').click(); + await page.getByRole('textbox').nth(2).fill(firstGoods.num); + await page.getByRole('button', { name: /搜\s索/ }).click(); + await expect(async () => { + await page.getByLabel(firstGoods.name).uncheck(); + await page.getByLabel(firstGoods.name).check(); + await expect($goodsViewSelect).toBeVisible({ timeout: 2000 }); + }).toPass({ timeout: 30_000 }); + await page.getByRole('button', { name: '确定选择' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('购买项目/卖品').click(); + await page.getByRole('option', { name: '消耗项目' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('消耗项目').click(); + await page.getByRole('option', { name: '开卡' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('开卡').click(); + await page.getByRole('option', { name: '充值' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('充值').click(); + await page.getByRole('option', { name: '还款' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('还款').click(); + await page.getByRole('option', { name: '换项目/产品' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('换项目/产品').click(); + await page.getByRole('option', { name: '抹欠款' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('抹欠款').click(); + await page.getByRole('option', { name: '补录' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('补录').click(); + await page.getByRole('option', { name: '跨店帮忙' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('跨店帮忙').click(); + await page.getByRole('option', { name: '会员卡退卡' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.reload(); + await page.getByRole('button').click(); + await page.getByPlaceholder('输入流水单号搜索').fill(billNo); + await page.locator('.search_btn > svg').click(); + await expect($flowChart).toBeVisible(); + }); + }); + + test('查询业绩流水列表明细', async ({ page, homeNavigation, createCustomer, wasteBookBusinessRecordPage }) => { + // 创建顾客A随机姓名 手机 + const ca = createCustomer; + const usernameA = ca.username; + const phoneA = ca.phone; + + await test.step('开单购买会员卡', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /^开\s单$/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(phoneA); + await page + .locator('.search_text') + .filter({ hasText: /^搜\s索$/ }) + .click(); + // 选择会员 + await page.locator('.custom_content').getByText(usernameA).click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await page.locator('.custom_content').waitFor(); + // 点击开卡 + const ActivateCards = page.getByRole('button', { name: /^开\s卡$/ }); + if (await ActivateCards.isVisible()) { + await page.getByRole('button', { name: /^开\s卡$/ }).click(); + } else { + await page.locator('.more').first().click(); + await page.getByText('去开卡').click(); + } + // 选择会员卡A + await page + .locator('.memberCard_box > .needsclick') + .getByText(/^会员卡$/) + .click(); + // 结算 + await page.getByRole('button', { name: '去结算' }).click(); + // 选择现金支付 + await page.getByText('现金').click(); + // 取消推送消费提醒 + await page.getByLabel('推送消费提醒').uncheck(); + // 取消结算签字 + await page.getByLabel('结算签字').uncheck(); + // 结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + // 会员协议签署确认(需判断 常报错) + try { + await page.locator('.modal_title', { hasText: '会员协议签署确认' }).waitFor(); + await page.getByRole('button', { name: /跳\s过/ }).click(); + } catch { + console.log('无会员协议签署'); + } + await expect(page.locator('.ant-message')).toContainText('结算成功'); + }); + + await test.step('进入顾客详情', async () => { + await page.reload(); + await homeNavigation.gotoModule('顾客'); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(phoneA); + await page.getByText('搜索', { exact: true }).click(); + await page.getByText(usernameA).click(); + // 点击进入详情 + await page.locator('.user_info_head .user_name', { hasText: usernameA }).last().click(); + }); + + // 定义随机单号 + const ReceiptNum = faker.helpers.fromRegExp(/1[3-9][0-9]{8}/); + const Cash = '30'; + const CardAmount = '10'; + const FreeAmount = '20'; + const Arrears = '20'; + // 选择员工A + const employee1 = Employees.FirstShop.Employee_6.name; + await test.step('开单消耗项目,选择员工,混合支付,随机单号', async () => { + await page.locator('span').filter({ hasText: '去开单' }).first().click(); + await page.getByText(ProjectName.Projects.Projects_17.num).click(); + // 点击添加员工 + await page.locator('#buyList').getByRole('button').nth(1).click(); + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + // 消耗该项目1次 + await page.locator('.commodity_item').last().click(); + // 点击添加员工 + await page.locator('.use_item .staff_btn').click(); + // 选择员工A + await page.locator('.hand_txt .name_txt').getByText(employee1).click(); + // 确认 + await page.getByRole('button', { name: /^确\s认$/ }).click(); + await page + .locator('div') + .filter({ hasText: /^结 算$/ }) + .click(); + // 点击修改单号 + await page.locator('.input165').click(); + // 输入随机单号 + await page.getByPlaceholder('请输入内容').fill(ReceiptNum); + // 确认 + await page.locator('.tools_icon').last().click(); + //点击混合支付 + await page.locator('.paytype').first().getByText('混合支付').click(); + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + //点击卡金 + await rightPaymentInfoItem.getByText('卡金', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.money_discount_input').fill(CardAmount); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //点击赠金 + await rightPaymentInfoItem.getByText('赠金', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.money_discount_input').fill(FreeAmount); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //点击现金 + await rightPaymentInfoItem.getByText('现金', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + //输入金额 + await page.locator('.money_discount_input').fill(Cash); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //点击欠款 + await rightPaymentInfoItem.getByText('欠款', { exact: true }).click(); + //增加收款 + await page.getByRole('button', { name: '增加收款' }).click(); + + await page.locator('.money_discount_input').fill(Arrears); + //确认金额 + await page.locator('.sure .tools_icon').click(); + //取消推送消息提醒 + await page.getByLabel('推送消费提醒').uncheck(); + //取消结算签字 + await page.getByLabel('结算签字').uncheck(); + //结算 + await page.getByRole('button', { name: /^结\s算$/ }).click(); + }); + + await test.step('进入水单详情页,查看数据', async () => { + // 进入业绩流水 + await homeNavigation.gotoModule('流水'); + await wasteBookBusinessRecordPage.gotoSubPage('业绩流水'); + // 点击搜索水单 + await expect(async () => { + await page.getByRole('button').click(); + await page.locator('.searchButton .search_input .ant-input').waitFor({ timeout: 2000 }); + }).toPass(); + await page.locator('.searchButton .search_input .ant-input').fill(ReceiptNum); + await page.locator('.searchButton .search_input .search_btn').click(); + await page.locator('.loading_container').waitFor({ state: 'hidden' }); + await expect(page.locator('.m-table__fixed-left .m-dropdown-link').first()).toContainText(ReceiptNum); + + // 现金业绩 + const CashPerformance = KeepOnlyNumbers( + await page + .locator('.m-table__body-wrapper .main-table-body_tr') + .nth(1) + .locator('.is-right') + .first() + .innerText(), + ); + const Cashs = Number(Cash) * 0.8 + ''; + console.log('现金业绩' + CashPerformance); + + // 划卡业绩 + const CardPerformance = KeepOnlyNumbers( + await page + .locator('.m-table__body-wrapper .main-table-body_tr') + .nth(1) + .locator('.is-right') + .nth(1) + .innerText(), + ); + console.log('划卡业绩' + CardPerformance); + + // 划赠金业绩 + const FreePerformance = KeepOnlyNumbers( + await page + .locator('.m-table__body-wrapper .main-table-body_tr') + .nth(1) + .locator('.is-right') + .nth(2) + .innerText(), + ); + console.log('赠金业绩' + FreePerformance); + + // 消耗业绩 + const ExpendPerformance = KeepOnlyNumbers( + await page + .locator('.m-table__body-wrapper .main-table-body_tr') + .nth(0) + .locator('.is-right') + .nth(4) + .innerText(), + ); + const ProjectPrice = ProjectName.Projects.Projects_17.Price; // 项目价格 + const ProjectPrices = Number(ProjectPrice) * 0.8 + ''; // 会员卡打8折 + console.log('消耗业绩' + ExpendPerformance); + + // 消耗 员工 + const ExpendEmployee = await page + .locator('.m-table__body-wrapper .main-table-body_tr') + .nth(0) + .locator('.billUser span') + .last() + .innerText(); + console.log('消耗 员工' + ExpendEmployee); + + // 购买 员工 + const BuyEmployee = await page + .locator('.m-table__body-wrapper .main-table-body_tr') + .nth(1) + .locator('.billUser span') + .last() + .innerText(); + console.log('购买 员工' + BuyEmployee); + + expect(CashPerformance).toBe(Cashs); // 现金业绩 + expect(CardPerformance).toBe(CardAmount); // 划卡业绩 + expect(FreePerformance).toBe(FreeAmount); // 划赠金业绩 + expect(ExpendPerformance).toBe(ProjectPrices); // 消耗业绩 + expect(ExpendEmployee).toBe(employee1); // 消耗 员工 + expect(BuyEmployee).toBe(employee1); // 购买 员工 + }); + }); +}); + +test.describe('对账流水', () => { + test('根据条件搜索对账流水', async ({ page, homeNavigation, createCustomer, wasteBookBusinessRecordPage }) => { + const customer = createCustomer; + const project = { no: '100018', name: '苹果精萃护理', shortName: '精萃护理', price: '980' }; + /**@type {string} 单号*/ + let billNo; + await test.step('选择顾客开单,结算使用银联、支付宝、微信、欠款、现金', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + await page.locator('.project_list .number', { hasText: project.no }).click(); + await page.locator('.pay_btn', { hasText: /^结\s算$/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + page.locator('.left .paymentmain .paymentInfoItem').filter({ + hasText: '混合支付', + }); + await leftPaymentInfoItem.filter({ hasText: '混合支付' }).click(); + // 银联 + await rightPaymentInfoItem.filter({ hasText: '银联' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 支付宝 + await rightPaymentInfoItem.filter({ hasText: '支付宝' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 微信 + await rightPaymentInfoItem.filter({ hasText: '微信' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 欠款 + await rightPaymentInfoItem.filter({ hasText: '欠款' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 现金 + await rightPaymentInfoItem.filter({ hasText: '现金' }).first().click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + + // 等待 /bill 请求完成并获取流水单号 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && res.status() === 200; + }), + page.getByRole('button', { name: /结\s算/ }).click(), + ]); + + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + expect(billNo).not.toBeNull(); + }); + + await test.step('根据筛选条件进行搜索水单', async () => { + await Promise.all([homeNavigation.gotoModule('流水'), page.waitForLoadState()]); + await wasteBookBusinessRecordPage.gotoSubPage('对账流水'); + + // 流水单 + const $flowChart = page.getByRole('cell', { name: billNo, exact: true }).first(); + + await page.getByTitle('全部水单类型').click(); + await page.getByTitle('全部记账流水').click(); + await page.getByRole('option', { name: '有记账无收款流水' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('全部水单类型').click(); + await page.getByRole('option', { name: '购买项目/卖品' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('购买项目/卖品').click(); + await page.getByRole('option', { name: '开卡' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('开卡').click(); + await page.getByRole('option', { name: '充值' }).click(); + await expect($flowChart).not.toBeVisible(); + + await page.getByTitle('充值').click(); + await page.getByRole('option', { name: '全部水单类型' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('全部支付方式').click(); + await page.getByRole('option', { name: '现金' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('现金').click(); + await page.getByRole('option', { name: '银联' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('银联').click(); + await page.getByRole('option', { name: '微信' }).click(); + await expect($flowChart).toBeVisible(); + + await page.getByTitle('微信').click(); + await page.getByRole('option', { name: '支付宝' }).click(); + await expect($flowChart).toBeVisible(); + + await page.reload(); + await page.getByRole('button').click(); + await page.getByPlaceholder('输入流水单号搜索').fill(billNo); + await page.locator('.search_btn > svg').click(); + await expect($flowChart).toBeVisible(); + }); + }); + test('对账流水只展示现金、支付宝、银联、微信', async ({ page, homeNavigation, createCustomer }) => { + const project = { no: '100018', name: '苹果精萃护理', shortName: '精萃护理', price: '980' }; + + const customer = createCustomer; + + /**@type {string} 单号*/ + let billNo; + + await test.step('选择顾客开单,结算使用银联、支付宝、微信、欠款、现金', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + await page.locator('.project_list .number', { hasText: project.no }).click(); + await page.locator('.pay_btn', { hasText: /^结\s算$/ }).click(); + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + + const rightPaymentInfoItem = page.locator('.right .paymentmain .paymentInfoItem'); + const leftPaymentInfoItem = page.locator('.left .paymentmain .paymentInfoItem'); + page.locator('.left .paymentmain .paymentInfoItem').filter({ + hasText: '混合支付', + }); + await leftPaymentInfoItem.filter({ hasText: '混合支付' }).click(); + // 银联 + await rightPaymentInfoItem.filter({ hasText: '银联' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 支付宝 + await rightPaymentInfoItem.filter({ hasText: '支付宝' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 微信 + await rightPaymentInfoItem.filter({ hasText: '微信' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 欠款 + await rightPaymentInfoItem.filter({ hasText: '欠款' }).click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.popup_content input').fill('1'); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + // 现金 + await rightPaymentInfoItem.filter({ hasText: '现金' }).first().click(); + await page.getByRole('button', { name: '增加收款' }).click(); + await page.locator('.number_tr').nth(2).getByRole('button').nth(3).click(); + + // 等待 /bill 请求完成并获取流水单号 + const [response] = await Promise.all([ + page.waitForResponse(async res => { + return res.url().includes('/bill') && res.status() === 200; + }), + page.getByRole('button', { name: /结\s算/ }).click(), + ]); + + const responseBody = await response.json(); + billNo = responseBody?.content?.billNo; + expect(billNo).not.toBeNull(); + }); + + await test.step('进入流水-对账流水', async () => { + // 进入流水模块 + await homeNavigation.gotoModule('流水'); + await page.locator('.ant-dropdown-link', { hasText: '营业记录' }).click(); + await Promise.all([ + await page.getByRole('menuitem', { name: '对账流水' }).click(), + await page.waitForResponse( + response => response.url().includes('/payment_flow') && response.status() === 200, + ), + ]); + + // 拿取现金列的列数 + let index; + const thLocator = page.locator('.m-table__header tr').last().locator('th'); + const headers = await thLocator.allInnerTexts(); + index = headers.findIndex(headerText => headerText.includes('支付方式')); + if (index !== -1) { + console.log(`"支付方式" 列是第 ${index + 1} 列`); + } else { + throw new Error('没有找到支付方式列'); + } + + const paymentArray = ( + await page + .locator('.main-table-body_tr ', { hasText: billNo }) + .locator(`td:nth-child(${index + 1})`) + .allInnerTexts() + ).map(i => i.trim()); + console.log(paymentArray); + + expect.soft(paymentArray).toContain('银联'); + expect.soft(paymentArray).toContain('现金'); + expect.soft(paymentArray).toContain('支付宝'); + expect.soft(paymentArray).toContain('微信'); + expect.soft(paymentArray).toContain('微信'); + expect(paymentArray).not.toContain('欠款'); + }); + }); +}); + +test.describe('日结单', () => { + /** + * 获取员工列的各项数据 + * @param {import('@playwright/test').Page} page + * @param {number} index 员工列 + * @param {{ 'name': string, 'lastPrice': number, 'price': number }[]} data 员工数据 + */ + async function getColumnPrice(page, index, data) { + const columnName = ['现金', '消耗', '划卡', '客数', '客次', '拓客', '留客', '项目数', '卖品', '总耗业绩']; + for (const name of columnName) { + const price = await page + .locator('.m-table__body-wrapper tr', { hasText: name }) + .locator('td') + .nth(index) + .locator('.hlknum') + .innerText(); + console.log(`${name} -- ${price}`); + + const item = data.find(item => item.name === name); + if (!item) { + throw new Error(`没有找到 ${name} 数据`); + } + item.lastPrice = item.price; + item.price = price.trim() === '--' ? 0 : Number(price); + } + } + + test('根据条件搜索日结单', async ({ page, homeNavigation }) => { + const employee_1 = staffData.firstStore.firstSector.employee_2; + const employee_2 = staffData.firstStore.secondSector.employee_2; + const firstSectorName = staffData.firstStore.firstSector.name; + const secondSectorName = staffData.firstStore.secondSector.name; + + await test.step('进入日结单模块', async () => { + await homeNavigation.gotoModule('流水'); + await page.getByText('日结单').first().click(); + await expect(page.getByText('查看更多汇总数据')).toBeVisible(); + + await expect(page.getByRole('cell', { name: employee_1.name })).toBeVisible(); + await expect(page.getByRole('cell', { name: employee_2.name })).toBeVisible(); + }); + + await test.step('切换到一部门查看员工A', async () => { + await page.locator('.shop-picker-store > .icon > svg').click(); + await page.getByLabel(firstSectorName).check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.getByRole('cell', { name: employee_1.name })).toBeVisible(); + await expect(page.getByRole('cell', { name: employee_2.name })).not.toBeVisible(); + }); + + await test.step('切换到二部门查看员工B', async () => { + await page.locator('.shop-picker-store > .icon > svg').click(); + await page.getByLabel(secondSectorName).check(); + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.getByRole('cell', { name: employee_1.name })).not.toBeVisible(); + await expect(page.getByRole('cell', { name: employee_2.name })).toBeVisible(); + }); + }); + + test('查询日结单明细', async ({ page, homeNavigation, createCustomer }) => { + // 使用的员工 + const employee = staffData.firstStore.firstSector.employee_1; + const project = { no: '100018', name: '苹果精萃护理', shortName: '精萃护理', price: 980 }; + + // 当前员工的日结单数据初始值 + const employeeData = [ + { name: '现金', price: 0, lastPrice: 0 }, + { + name: '消耗', + price: 0, + lastPrice: 0, + }, + { name: '划卡', price: 0, lastPrice: 0 }, + { + name: '客数', + price: 0, + lastPrice: 0, + }, + { name: '客次', price: 0, lastPrice: 0 }, + { + name: '拓客', + price: 0, + lastPrice: 0, + }, + { name: '留客', price: 0, lastPrice: 0 }, + { + name: '项目数', + price: 0, + lastPrice: 0, + }, + { name: '卖品', price: 0, lastPrice: 0 }, + { name: '总耗业绩', price: 0, lastPrice: 0 }, + ]; + + const customer = createCustomer; + + // 拿取员工列的列数 + let index; + await test.step('进入流水-日结单,获取员工数据', async () => { + await homeNavigation.gotoModule('流水'); + await page.locator('.top_tab').getByText('日结单').first().click(); + await page + .getByRole('row', { + name: '现金', + exact: true, + }) + .locator('div') + .nth(1) + .waitFor(); + + const thLocator = page.locator('.m-table__header-wrapper tr').last().locator('th'); + await thLocator.last().waitFor(); + const headers = await thLocator.allInnerTexts(); + index = headers.findIndex(headerText => { + return headerText.includes(employee.name) && headerText.includes(String(employee.id)); + }); + if (index !== -1) { + console.log(`"${employee.name}" 列是第 ${index + 1} 列`); + } else { + throw new Error(`没有找到${employee.name}, ${employee.id}列`); + } + + // 获取员工各个行的值 + await getColumnPrice(page, index, employeeData); + }); + + await test.step('开单结算,选择员工', async () => { + await homeNavigation.gotoModule('收银'); + await page.getByRole('button', { name: /开\s单/ }).click(); + await page.getByPlaceholder('姓名(拼音首字)、手机号、档案号搜索').fill(customer.phone); + await page.locator('.ant-input-suffix', { hasText: '搜索' }).click(); + await page.locator('.member_list .phone', { hasText: customer.phone }).click(); + // 购买并消费项目A + await page.locator('.project_list .number', { hasText: project.no }).click(); + await page.locator('#shoppingCart .commodity_list li').first().click(); + + // 购买项目选择员工A + await page.locator('.buy_item').first().locator('.buy_staff').getByRole('button').click(); + await page + .locator('.check_row', { + hasText: employee.name, + }) + .getByRole('checkbox') + .check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 消耗项目选择员工A + await page.locator('.use_item').first().locator('.use_staff').getByRole('button').click(); + await page + .locator('.check_row', { + hasText: employee.name, + }) + .getByRole('checkbox') + .check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + await page.locator('.pay_btn', { hasText: /^结\s算$/ }).click(); + + // 去使用现金结算 + await page.getByLabel('推送消费提醒').uncheck(); + await page.getByLabel('结算签字').uncheck(); + await page.locator('.left .paymentInfoItem', { hasText: '现金' }).click(); + await page.getByRole('button', { name: /结\s算/ }).click(); + }); + + await test.step('进入流水-日结单,比较员工数据变化', async () => { + await homeNavigation.gotoModule('流水'); + await page.locator('.top_tab').getByText('日结单').first().click(); + await page + .getByRole('row', { + name: '现金', + exact: true, + }) + .locator('div') + .nth(1) + .waitFor(); + + await getColumnPrice(page, index, employeeData); + + // 购买0.5 + const checks = [ + { name: '现金', expected: project.price * 0.5 }, + { + name: '消耗', + expected: project.price * 0.6, + }, + { name: '客数', expected: 1 }, + { name: '客次', expected: 1 }, + { + name: '拓客', + expected: 1, + }, + { name: '留客', expected: 0 }, + { name: '项目数', expected: 1 }, + { + name: '卖品', + expected: 0, + }, + { name: '总耗业绩', expected: project.price * 0.6 }, + ]; + + checks.forEach((check, index) => { + const item = employeeData.find(i => i.name === check.name); + if (!item) { + throw new Error(`没有找到 ${check.name} 数据`); + } + if (index < checks.length - 1) { + expect.soft(parseFloat((item.price - item.lastPrice).toFixed(2))).toBe(check.expected); + } else { + expect(parseFloat((item.price - item.lastPrice).toFixed(2))).toBe(check.expected); + } + }); + }); + }); +}); diff --git a/tests/touch/staff_goal.spec.ts b/tests/touch/staff_goal.spec.ts new file mode 100644 index 0000000..45521ce --- /dev/null +++ b/tests/touch/staff_goal.spec.ts @@ -0,0 +1,276 @@ +// @ts-check +import { test, expect } from '@/fixtures/staff_common.js'; +import { Employees } from '@/fixtures/userconfig.js'; + +test.describe('目标-目标设置', () => { + test('门店设置一店', async ({ page, baseURL, numberInput }) => { + const goal = '100'; //目标金额 + let verifys; //校验值 + const employeeA = Employees.FirstShop.Employee_3.name; + + await page.goto(baseURL ?? ''); + + await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(process.env.staff1_account ?? ''); + await page.locator('.pass_svg').waitFor(); + await page.getByRole('textbox', { name: '请输入登录密码' }).fill(process.env.staff1_password ?? ''); + await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter'); + await page.getByRole('button', { name: '同意并登录' }).click(); + // await page.getByText('美容顾问 一店顾问').click(); + + // End of authentication steps. + await expect(page.getByText('收银', { exact: true })).toBeVisible(); + await page.waitForLoadState('networkidle'); + + await page + .locator('div') + .filter({ hasText: /^目标$/ }) + .first() + .click(); + await page.getByRole('button', { name: '设置目标' }).click(); + // 员工A内行数 + let nowRowA = 0; + const allTrA = page.locator('.popupComTableStyle .m-table__fixed-left tbody tr'); + const countA = await allTrA.count(); + // 获取项目处于第几行 + for (let i = 0; i < countA; i++) { + const trA = allTrA.nth(i); + const employee = await trA.locator('.popupEmployeeName').first().innerText(); + console.log(employee); + if (employee.includes(employeeA)) { + nowRowA = i; + break; + } + } + + await page + .locator('.popupComTableStyle .m-table__body .main-table-body_tr') + .nth(nowRowA) + .locator('.m-table-cell') + .nth(1) + .click(); + await numberInput.setValue(Number(goal)); + await numberInput.confirmValue(); + await page.getByRole('button', { name: '保 存' }).click(); + + // 员工A外行数 + let nowRowB = 0; + const allTrB = page.locator('.tableStyle .m-table__fixed-left tbody tr'); + const countB = await allTrB.count(); + // 获取项目处于第几行 + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const employee = await trB.locator('.employeeName').first().innerText(); + console.log(employee); + if (employee.includes(employeeA)) { + nowRowB = i; + break; + } + } + + const verify = await page + .locator('.tableStyle .m-table__body-wrapper tbody tr') + .nth(nowRowB) + .locator('.is-right .targetValue') + .nth(0) + .innerText(); + if (verify === '--') { + verifys = 0; + } else { + verifys = verify; + } + // 对比输入的目标 + expect.soft(verifys).toBe(goal); + + // 清理 + await page.getByRole('button', { name: '设置目标' }).click(); + + // 员工A内行数 + for (let i = 0; i < countA; i++) { + const trA = allTrA.nth(i); + const employee = await trA.locator('.popupEmployeeName').first().innerText(); + console.log(employee); + if (employee.includes(employeeA)) { + nowRowA = i; + break; + } + } + + await page + .locator('.popupComTableStyle .m-table__body .main-table-body_tr') + .nth(nowRowA) + .locator('.m-table-cell') + .nth(1) + .click(); + await numberInput.setValue(0); + await numberInput.confirmValue(); + await page.getByRole('button', { name: '保 存' }).click(); + }); + + test('门店设置二店', async ({ page, baseURL }) => { + await page.goto(baseURL ?? ''); + + await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(process.env.staff2_account ?? ''); + await page.locator('.pass_svg').waitFor(); + await page.getByRole('textbox', { name: '请输入登录密码' }).fill(process.env.staff2_password ?? ''); + await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter'); + await page.getByRole('button', { name: '同意并登录' }).click(); + try { + await page.locator('.versionModal_main_content').waitFor({ timeout: 5000 }); + await page.locator('.versionModal_main_content .close_icon').click(); + } catch { + console.log('无版本更新'); + } + await expect(page.getByText('收银', { exact: true })).toBeVisible(); + await page.waitForLoadState('networkidle'); + + await page + .locator('div') + .filter({ hasText: /^目标$/ }) + .first() + .click(); + await page.getByRole('button', { name: '设置目标' }).waitFor(); + await page.getByRole('button', { name: '设置目标' }).click(); + await expect.soft(page.locator('.popup_content', { hasText: '温馨提示' })).toBeVisible(); + const WarmReminder = await page.locator('.popup_content .content div').innerText(); + console.log(WarmReminder); + expect + .soft(WarmReminder) + .toBe('亲,该门店(AT测试二店)尚未开通目标管理功能,您不可为员工设置目标,也无法跟进员工目标完成情况。'); + await page.getByRole('button', { name: '我知道了' }).click(); + }); + + test('员工设置', async ({ page, numberInput }) => { + // A员工李娜账号密码 + const accountA = '139876543'; + const passwordA = 'a123456'; + // B员工刘强账号密码 + const accountB = '134987612'; + const passwordB = 'a123456'; + const employeeA = Employees.FirstShop.Employee_4.name; // 刘强 + const employeeB = Employees.FirstShop.Employee_8.name; // 刘强 + const employeeC = Employees.FirstShop.Employee_9.name; // 周萍 + const goalB = '100'; + const goalC = '200'; + let verifyB; //校验B员工 + let verifyC; //校验C员工 + let verify; //汇总 + await page.goto(process.env.BASE_URL ?? ''); + + await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(accountA); + await page.locator('.pass_svg').waitFor(); + await page.getByRole('textbox', { name: '请输入登录密码' }).fill(passwordA); + await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter'); + await page.getByRole('button', { name: '同意并登录' }).click(); + // await page.getByText('美容顾问 一店顾问').click(); + + // End of authentication steps. + await expect(page.getByText('收银', { exact: true })).toBeVisible(); + await page.waitForLoadState('networkidle'); + + await page + .locator('div') + .filter({ hasText: /^目标$/ }) + .first() + .click(); + await page.getByRole('button', { name: '设置目标' }).click(); + + // 员工B内行数 + let nowRowB = 0; + const allTrB = page.locator('.popupComTableStyle .m-table__fixed-left tbody tr'); + const countB = await allTrB.count(); + // 获取项目处于第几行 + for (let i = 0; i < countB; i++) { + const trB = allTrB.nth(i); + const employee = await trB.locator('.popupEmployeeName').first().innerText(); + console.log(employee); + if (employee.includes(employeeB)) { + nowRowB = i; + break; + } + } + + const code = page.locator('.popupComTableStyle .m-table__body .main-table-body_tr'); + await code.nth(nowRowB).locator('.m-table-cell').nth(1).click(); + await numberInput.setValue(Number(goalB)); + await numberInput.confirmValue(); + + // 员工C内行数 + let nowRowC = 0; + const allTrC = page.locator('.popupComTableStyle .m-table__fixed-left tbody tr'); + const countC = await allTrC.count(); + // 获取项目处于第几行 + for (let i = 0; i < countC; i++) { + const trA = allTrC.nth(i); + const employee = await trA.locator('.popupEmployeeName').first().innerText(); + console.log(employee); + if (employee.includes(employeeC)) { + nowRowC = i; + break; + } + } + + // 判断李娜在目标设置不存在 + await expect( + page + .locator('.popupComTableStyle .m-table__fixed-left tbody tr') + .locator('.popupEmployeeName', { hasText: employeeA }), + ).not.toBeVisible(); + + await code.nth(nowRowC).locator('.m-table-cell').nth(1).click(); + await numberInput.setValue(Number(goalC)); + await numberInput.confirmValue(); + await page.getByRole('button', { name: /保\s存/ }).click(); + + // 员工B外行数 + const allTrBB = page.locator('.tableStyle .m-table__fixed-left tbody tr'); + const nowRowBB = await allTrBB.allInnerTexts().then(text => { + return text.findIndex(value => value.includes(employeeB)); + }); + if (nowRowBB === -1) { + throw new Error('员工B不存在'); + } + + const codes = page.locator('.tableStyle .m-table__body-wrapper tbody tr'); + // 对比输入的目标 + await expect(codes.nth(nowRowBB).locator('.is-right .targetValue').nth(0)).toContainText(goalB); + + // 员工C外行数 + const allTrCC = page.locator('.tableStyle .m-table__fixed-left tbody tr'); + const nowRowCC = await allTrCC.allInnerTexts().then(text => { + return text.findIndex(value => value.includes(employeeC)); + }); + + // 对比输入的目标 + await expect(codes.nth(nowRowCC).locator('.is-right .targetValue').nth(0)).toContainText(goalC); + + // 总数 + const allTr = page.locator('.tableStyle .m-table__fixed-left tbody tr'); + let nowRow = await allTr.allInnerTexts().then(text => { + return text.findIndex(value => value.includes('目标汇总')); + }); + + // 对比输入的目标 + await expect + .soft(codes.nth(nowRow).locator('.is-right .targetValue').nth(0)) + .toContainText(`${Number(goalB) + Number(goalC)}`); + + // 退出登录 + await page.locator('.exit_btn').click(); + await page.locator('.comfirm_btn').click(); + + await page.goto(process.env.BASE_URL ?? ''); + + await page.getByRole('textbox', { name: '请输入您的手机号码' }).fill(accountB); + await page.locator('.pass_svg').waitFor(); + await page.getByRole('textbox', { name: '请输入登录密码' }).fill(passwordB); + await page.getByRole('textbox', { name: '请输入登录密码' }).press('Enter'); + await page.waitForLoadState('networkidle'); + + await page + .locator('div') + .filter({ hasText: /^目标$/ }) + .first() + .click(); + await expect(page.locator('.m-table__fixed-left .m-table-cell', { hasText: '日期' })).toBeVisible(); + }); +}); diff --git a/tests/touch/staff_inventory.spec.ts b/tests/touch/staff_inventory.spec.ts new file mode 100644 index 0000000..408c4e8 --- /dev/null +++ b/tests/touch/staff_inventory.spec.ts @@ -0,0 +1,236 @@ +import { faker } from '@faker-js/faker/locale/zh_CN'; +import { test, expect } from '@/fixtures/staff_common.js'; +import { getListIndexForTargetElement } from '@/utils/utils.js'; + +test.describe('调货管理', () => { + test('门店要货', async ({ page, homeNavigation }) => { + // 使用产品 + const goods = { + a: { no: 'aa100007', name: '肌因能量套', unitPrice: 10, quantity: 5 }, + b: { no: 'aa100008', name: '肌因赋活尊享套', unitPrice: 15, quantity: 8 }, + }; + // 备注 + const remark = '调货管理-门店要货' + faker.helpers.fromRegExp(/[0-9]{3}/); + let quantity = 0; // 数量 + let unitPrice = 0; // 单价 + let subtotal = 0; // 小计 + let totalQuantity = 0; // 总数量 + let totalPrice = 0; // 总价 + await test.step('门店保存要货单', async () => { + await homeNavigation.gotoModule('库存'); + await page.locator('.top_tab').getByText('调货管理').click(); + + // 展示单价和小计 + await page.locator('.price_set').click(); + await page.locator('.set_btn').getByRole('checkbox').check(); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 选择产品a,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.a.no }).click(); + const goodsATr = page.locator('.bill_report tr', { hasText: goods.a.name }); + await goodsATr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.quantity)); + await page.locator('.number_tr button').nth(11).click(); + await goodsATr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.unitPrice)); + await page.locator('.number_tr button').nth(11).click(); + + // 选择产品b,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.b.no }).click(); + const goodsBTr = page.locator('.bill_report tr', { hasText: goods.b.name }); + await goodsBTr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.quantity)); + await page.locator('.number_tr button').nth(11).click(); + await goodsBTr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.unitPrice)); + await page.locator('.number_tr button').nth(11).click(); + + // 设置备注 + await page.getByText('备注').click(); + await page.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await page.getByRole('button', { name: /确\s认/ }).click(); + + // 保存要货单 + await page.getByRole('button', { name: /保\s存/ }).click(); + await expect(page.getByRole('cell', { name: '门店要货备注' })).toBeInViewport(); + }); + + await test.step('查看保存的要货单', async () => { + // 查看明细单据 + const tableTrList = page.locator('.table_inner .main-table-body_tr'); + const targetTr = tableTrList.filter({ hasText: remark }).filter({ hasText: '未提交' }); + const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList); + const fixedCell = page + .locator('.m-table-fixed-body') + .getByRole('cell', { name: '明细编辑删除' }) + .nth(targetIndex); + await fixedCell.getByText('明细').click(); + + // 拿取商品a的数据 + const goodsATr = page.locator('.popup_content tr', { hasText: goods.a.name }); + quantity = Number(await goodsATr.locator('td').nth(2).innerText()); + unitPrice = Number(await goodsATr.locator('td').nth(6).innerText()); + subtotal = Number(await goodsATr.locator('td').nth(7).innerText()); + totalPrice += unitPrice * quantity; + totalQuantity += quantity; + + // 判断商品a的数据 + expect.soft(quantity).toBe(goods.a.quantity); + expect.soft(unitPrice).toBe(goods.a.unitPrice); + expect(subtotal).toBe(goods.a.quantity * goods.a.unitPrice); + + // 拿取商品b的数据 + const goodsBTr = page.locator('.popup_content tr', { hasText: goods.b.name }); + quantity = Number(await goodsBTr.locator('td').nth(2).innerText()); + unitPrice = Number(await goodsBTr.locator('td').nth(6).innerText()); + subtotal = Number(await goodsBTr.locator('td').nth(7).innerText()); + totalPrice += unitPrice * quantity; + totalQuantity += quantity; + + // 判断商品b的数据 + expect.soft(quantity).toBe(goods.b.quantity); + expect.soft(unitPrice).toBe(goods.b.unitPrice); + expect.soft(subtotal).toBe(goods.b.quantity * goods.b.unitPrice); + + // 判断合计数量、合计金额 + expect.soft(totalPrice).toBe(goods.b.quantity * goods.b.unitPrice + goods.a.quantity * goods.a.unitPrice); + expect(totalQuantity).toBe(goods.a.quantity + goods.b.quantity); + + // 关闭弹窗 + await page.locator('.title > .close_icon > svg').click(); + }); + + await test.step('门店提交要货单', async () => { + // 查看明细单据 + let tableTrList = page.locator('.table_inner .main-table-body_tr'); + let targetTr = tableTrList.filter({ hasText: remark }).filter({ hasText: '未提交' }); + let targetIndex = await getListIndexForTargetElement(targetTr, tableTrList); + let fixedCell = page + .locator('.m-table-fixed-body') + .getByRole('cell', { name: '明细编辑删除' }) + .nth(targetIndex); + await fixedCell.getByText('编辑').click(); + + await expect(page.getByText('要货单').nth(1)).toBeInViewport(); + + // 选择产品a,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.a.no }).click(); + let goodsTr = page.locator('.bill_report tr', { hasText: goods.a.name }); + await goodsTr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.quantity * 2)); + await page.locator('.number_tr button').nth(11).click(); + await goodsTr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.a.unitPrice * 2)); + await page.locator('.number_tr button').nth(11).click(); + + // 选择产品b,设置产品数量和单价 + await page.locator('.panel tr', { hasText: goods.b.no }).click(); + goodsTr = page.locator('.bill_report tr', { hasText: goods.b.name }); + await goodsTr.getByRole('spinbutton').first().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.quantity * 2)); + await page.locator('.number_tr button').nth(11).click(); + await goodsTr.getByRole('spinbutton').last().click(); + await page.getByPlaceholder('请输入内容').fill(String(goods.b.unitPrice * 2)); + await page.locator('.number_tr button').nth(11).click(); + + // 保存要货单 + await page.getByRole('button', { name: /提\s交/ }).click(); + await expect(page.getByRole('cell', { name: '门店要货备注' })).toBeInViewport(); + }); + + await test.step('查看提交的要货单', async () => { + // 查看明细单据 + const tableTrList = page.locator('.table_inner .main-table-body_tr'); + const targetTr = tableTrList.filter({ hasText: remark }).filter({ hasText: '已提交' }); + const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList); + const fixedCell = page.locator('.m-table-fixed-body').getByRole('cell', { name: '明细' }).nth(targetIndex); + await fixedCell.getByText('明细').click(); + + totalQuantity = 0; + totalPrice = 0; + // 拿取商品a的数据 + let goodsTr = page.locator('.popup_content tr', { hasText: goods.a.name }); + quantity = Number(await goodsTr.locator('td').nth(2).innerText()); + unitPrice = Number(await goodsTr.locator('td').nth(6).innerText()); + subtotal = Number(await goodsTr.locator('td').nth(7).innerText()); + totalPrice += unitPrice * quantity; + totalQuantity += quantity; + + // 判断商品a的数据 + expect.soft(quantity).toBe(goods.a.quantity * 2); + expect.soft(unitPrice).toBe(goods.a.unitPrice * 2); + expect.soft(subtotal).toBe(goods.a.quantity * goods.a.unitPrice * 4); + + // 拿取商品b的数据 + goodsTr = page.locator('.popup_content tr', { hasText: goods.b.name }); + quantity = Number(await goodsTr.locator('td').nth(2).innerText()); + unitPrice = Number(await goodsTr.locator('td').nth(6).innerText()); + subtotal = Number(await goodsTr.locator('td').nth(7).innerText()); + totalPrice += unitPrice * quantity; + totalQuantity += quantity; + + // 判断商品b的数据 + expect.soft(quantity).toBe(goods.b.quantity * 2); + expect.soft(unitPrice).toBe(goods.b.unitPrice * 2); + expect.soft(subtotal).toBe(goods.b.quantity * goods.b.unitPrice * 4); + + // 判断合计数量、合计金额 + expect + .soft(totalPrice) + .toBe((goods.b.quantity * goods.b.unitPrice + goods.a.quantity * goods.a.unitPrice) * 4); + expect(totalQuantity).toBe((goods.a.quantity + goods.b.quantity) * 2); + }); + }); + + test('要货单', async ({ firstStaffPage, staffHomeNavigation, transferManagementPage }) => { + const goods = { no: 'aa100024', name: '茉莉精油', quantity: 10 }; + const remark = '门店要货单' + faker.string.numeric(3); + + await test.step('添加要货单', async () => { + await staffHomeNavigation.gotoModule('库存'); + await firstStaffPage.locator('.top_tab .tab_item').getByText('调货管理').click(); + await transferManagementPage.gotoSubPage('门店要货'); + await firstStaffPage + .getByRole('row', { name: goods.no }) + .and(firstStaffPage.getByRole('row', { name: goods.name })) + .click(); + const goodsTr = firstStaffPage.locator('.bill_report tr', { hasText: goods.name }); + await goodsTr.getByRole('spinbutton').first().click(); + await firstStaffPage.getByPlaceholder('请输入内容').fill(String(goods.quantity)); + await firstStaffPage.locator('.number_tr button').nth(11).click(); + + // 设置备注 + await firstStaffPage.getByText('备注').click(); + await firstStaffPage.getByPlaceholder('请输入1-100个字符备注内容').fill(remark); + await firstStaffPage.getByRole('button', { name: /确\s认/ }).click(); + + // 保存要货单 + await firstStaffPage.getByRole('button', { name: /保\s存/ }).click(); + // 跳转到要货单 + await expect(firstStaffPage.getByRole('cell', { name: '门店要货备注' })).toBeVisible(); + + const billGoodsTr = firstStaffPage.locator('.m-table__body-wrapper tr', { + hasText: remark, + }); + await expect(billGoodsTr).toBeVisible(); + }); + + await test.step('删除要货单', async () => { + const tableTrList = firstStaffPage.locator('.table_inner .main-table-body_tr'); + const targetTr = tableTrList.filter({ hasText: remark }); + const targetIndex = await getListIndexForTargetElement(targetTr, tableTrList); + + const fixedCell = firstStaffPage + .locator('.m-table-fixed-body') + .getByRole('cell', { name: '明细编辑删除' }) + .nth(targetIndex); + await fixedCell.getByText('删除').click(); + + await firstStaffPage.getByRole('button', { name: /确\s认/ }).click(); + const billGoodsTr = firstStaffPage.locator('.m-table__body-wrapper tr', { + hasText: remark, + }); + await expect(billGoodsTr).not.toBeInViewport(); + }); + }); +}); diff --git a/tests/utils/customer.ts b/tests/utils/customer.ts new file mode 100644 index 0000000..4452629 --- /dev/null +++ b/tests/utils/customer.ts @@ -0,0 +1,40 @@ +import { faker } from '@faker-js/faker/locale/zh_CN'; + +export type employee = { level: string, name: string }; + +interface CustomerOptions { + username?: string; + phone?: string; + archive?: string; + gender?: number; + source?: number; + birthday?: { year: number; month: number; day: number }; + remark?: string; + employees?: employee[]; +} + +export class Customer { + store: number; + department: number; + username: string; + phone: string; + archive: string; + gender: number; + birthday?: { year: number; month: number; day: number }; + source: number; + remark: string; + employees: employee[]; + + constructor(store: number, department: number, options: CustomerOptions = {}) { + this.store = store; + this.department = department; + this.username = options.username ?? faker.person.fullName(); + this.phone = options.phone ?? faker.helpers.fromRegExp(/1[3-9][0-9]{6}/); + this.archive = options.archive ?? ''; + this.gender = options.gender ?? 0; + this.source = options.source ?? 1; + this.birthday = options.birthday; + this.remark = options.remark ?? ''; + this.employees = options.employees ?? []; + } +} diff --git a/tests/utils/indexedDBUtils.ts b/tests/utils/indexedDBUtils.ts new file mode 100644 index 0000000..88eef63 --- /dev/null +++ b/tests/utils/indexedDBUtils.ts @@ -0,0 +1,132 @@ +export async function readIndexedDB() { + const databases = await indexedDB.databases(); + const allData: Array<{ databaseName: string; data: { key: IDBValidKey; value: any }[] }> = []; + + for (const db of databases) { + const databaseName = db.name!; + + try { + const dbInstance = await openDatabase(databaseName); + const transaction = dbInstance.transaction(dbInstance.objectStoreNames, 'readonly'); + const objectStore = transaction.objectStore(dbInstance.objectStoreNames[0]); + const storeData = await getAllDataFromObjectStore(objectStore); + + allData.push({ + databaseName, + data: storeData, + }); + + dbInstance.close(); + } catch (error) { + console.error(`Error reading database ${databaseName}:`, error); + } + } + + return allData; +} + +function openDatabase(databaseName: string): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(databaseName); + + request.onsuccess = event => resolve(event.target.result); + request.onerror = event => reject(event.target.error); + }); +} + +function getAllDataFromObjectStore(objectStore: IDBObjectStore): Promise> { + return new Promise((resolve, reject) => { + const storeData: Array<{ key: IDBValidKey; value: any }> = []; + const cursorRequest = objectStore.openCursor(); + + cursorRequest.onsuccess = event => { + const cursor = event.target.result; + if (cursor) { + storeData.push({ key: cursor.key, value: cursor.value }); + cursor.continue(); + } else { + resolve(storeData); + } + }; + + cursorRequest.onerror = event => reject(event.target.error); + }); +} + +export async function writeIndexedDB(jsonData) { + return new Promise((resolve, reject) => { + const processDatabase = async (database, objectStoreNames) => { + try { + // 动态获取数据库版本号 + const version = database.version || 2; + const openRequest = indexedDB.open(database.databaseName, version); + + // 处理数据库版本升级 + openRequest.onupgradeneeded = event => { + const db = event.target.result; + objectStoreNames.forEach(storeName => { + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName); + } + }); + }; + + // 处理数据库打开成功 + openRequest.onsuccess = async event => { + const db = event.target.result; + const table = database.data; + const transaction = db.transaction(objectStoreNames, 'readwrite'); + const objectStore = transaction.objectStore(objectStoreNames[0]); + + // 添加事务错误处理 + transaction.onerror = event => { + console.error('事务失败:', event.target.error); + reject('事务失败'); + }; + + let completedOperations = 0; + const totalOperations = table.length; + + // 使用 Promise.all 来确保所有的写入操作完成 + const writeOperations = table.map((item, index) => { + return new Promise((innerResolve, innerReject) => { + const addObject = objectStore.put(item.value, item.key); + addObject.onsuccess = () => { + completedOperations++; + if (completedOperations === totalOperations) { + innerResolve('数据写入成功'); + } + }; + addObject.onerror = event => { + console.error('数据添加失败:', event.target.error); + innerReject('数据添加失败'); + }; + }); + }); + + // 等待所有写入操作完成 + try { + await Promise.all(writeOperations); + resolve('所有数据写入完成'); + } catch (error) { + reject('数据写入失败'); + } + }; + + // 错误处理 + openRequest.onerror = event => { + console.error('数据库打开失败:', event.target.error); + reject('数据库打开失败'); + }; + } catch (error) { + reject('数据库操作失败: ' + error); + } + }; + + // 遍历所有数据库 + const objectStoreNames = ['keyvaluepairs', 'local-forage-detect-blob-support']; + Promise.all(jsonData.map(database => processDatabase(database, objectStoreNames))) + .then(() => resolve('所有数据库操作完成')) + .catch(error => reject('操作失败: ' + error)); + }); +} diff --git a/tests/utils/utils.js b/tests/utils/utils.js new file mode 100644 index 0000000..3e2fd7a --- /dev/null +++ b/tests/utils/utils.js @@ -0,0 +1,201 @@ +// 解析二维码 +const decodeImage = require('jimp').read; +const { readFile, unlinkSync } = require('fs'); +const qrcodeReader = require('qrcode-reader'); +const sharp = require('sharp'); +const Tesseract = require('tesseract.js'); + +/** + * 解析二维码 + * @param {*} pathSrc 图片路径 + * @returns Promise 二维码内容 + */ +export const decodeQR = function(pathSrc) { + // var filePath = path.resolve(__dirname, pathSrc); + return new Promise((resolve, reject) => { + readFile(pathSrc, function(err, fileBuffer) { + if (err) { + reject(err); + return; + } + decodeImage(fileBuffer, function(err, image) { + if (err) { + reject(err); + return; + } + let decodeQR = new qrcodeReader(); + decodeQR.callback = function(errorWhenDecodeQR, result) { + if (errorWhenDecodeQR) { + reject(errorWhenDecodeQR); + unlinkSync(pathSrc); + return; + } + if (!result) { + console.log('gone with wind'); + resolve(''); + unlinkSync(pathSrc); + } else { + resolve(result.result); + console.log(result.result); //结果 + unlinkSync(pathSrc); + } + }; + decodeQR.decode(image.bitmap); + }); + }); + }); +}; + +/** + * - ¥1500 -> { method: '', amount: 1500 } + * - 银联¥1500.0 -> { method: 银联, amount: 1500 } + * @param {string} amountText ¥1500 + * @returns {{ method: string, amount: number }} + */ +export const convertAmountText = function(amountText) { + let method = ''; + let amount = 0; + const text = amountText.replace('\n', ''); + const amountTextArray = text.match(/(\s)¥([0-9.]+)/); + if (amountTextArray) { + method = amountTextArray[1]; + amount = Number(amountTextArray[2]); + } + return { method: method, amount: amount }; +}; + +/** + * 返回列表中目标元素的index + * @param {import('@playwright/test').Locator} targetElement + * @param elementList + */ +export const getListIndexForTargetElement = async (targetElement, elementList) => { + return targetElement.evaluate((el, list) => { + return Array.from(list).indexOf(el); + }, await elementList.elementHandles()); +}; + +/** + * 保留数字(清空非数字) + * @param {*} str 仅保留数字 + * @returns + */ +export function KeepOnlyNumbers(str) { + return str.replace(/\D/g, ''); +} + +/** + * 保留中文和数字(清空符号) + * @param {*} str 仅保留中文和数字 + * @returns + */ +export function CleanPunctuation(str) { + return str.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ''); +} + +/** + * 等待指定接口加载完成 + * @param {import('@playwright/test').Page} page + * @param {string[]} apiArray 接口名称数组 + * @returns Promise + */ +export const waitSpecifyApiLoad = (page, apiArray) => { + if (apiArray === undefined || apiArray.length === 0) { + return Promise.resolve([]); + } + return Promise.all( + apiArray.map(api => page.waitForResponse(res => res.url().includes(api) && res.status() === 200)), + ); +}; + +/** + * 等待Locator元素内的文本值不再变化 + * @param {import('@playwright/test').Locator} locator + * @param {boolean} [waitForAnimations = false] 等待动画结束 + * @param {number} [sameTextCount = 20] 重复次数 + * @returns + */ +export const waitStable = async function(locator, waitForAnimations = false, sameTextCount = 20) { + await locator.evaluate( + async (element, { waitForAnimations, sameTextCount }) => { + const progressIsStable = async function(element, lastText = null, sameTextCounter = 0, iteration = 0) { + // 递归次数超过500次退出 + if (iteration > 500) { + throw new Error('超出了最大的递归次数'); + } + + // 等待动画结束 + if (waitForAnimations) { + await Promise.all(element.getAnimations().map(animation => animation.finished)); + } + + // 间隔15ms检测一次元素变化 + await new Promise(resolve => setTimeout(resolve, 15)); + + // 获取当前元素文本 + const text = element.innerText; + // 和上次元素的文本进行对比 + const sameText = text === lastText; + + if (sameText) { + // 和上次一致,则+1 + ++sameTextCounter; + } else { + // 和上次不一致,则归0 + sameTextCounter = 0; + } + + const isStable = sameTextCounter >= sameTextCount; + if (isStable) { + // 重复次数超过20次则推出循环 + return true; + } else { + // 否则继续递归 + return progressIsStable(element, text, sameTextCounter, ++iteration); + } + }; + // 进行递归 + return progressIsStable(element); + }, + { waitForAnimations, sameTextCount }, + ); + // 返回通过校验的locator + return locator; +}; + +/** + * 处理图像并进行验证码识别 + * @param {string} inputImagePath - 输入图像文件名(位于 .images 文件夹内) + * @param {string} outputImagePath - 输出处理后图像的文件名(位于 .images 文件夹内) + * @returns {Promise} - 返回识别的文本结果 + */ +export const processAndRecognizeCaptcha = async (inputImagePath, outputImagePath) => { + try { + // 图像处理 + await sharp(inputImagePath) + // .sharpen() + .modulate({ + brightness: 1.2, // 增加亮度 + contrast: 1.5, // 增强对比度 + }) + .resize(800) // 调整图像宽度为800像素,保持纵横比 + .grayscale() // 转换为灰度图 + .threshold(128) // 二值化,阈值设定为128 + .toFile(outputImagePath); + + console.log('图像处理完成:', outputImagePath); + + // 图像识别 + const { + data: { text }, + } = await Tesseract.recognize(outputImagePath, 'eng', { + langPath: './tessdata', + }); + + console.log('识别结果:', text.trim()); + return text.replace(/\s+/g, '').trim(); // 返回识别的文本结果 + } catch (err) { + console.error('处理或识别出错:', err); + throw err; // 将错误抛出以供调用者处理 + } +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3e6ce38 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "jsx": "react", + "strictNullChecks": true, + "strictFunctionTypes": true, + "sourceMap": true, + "baseUrl": ".", + "paths": { + "@/*":["tests/*"] + } + }, + "exclude": [ + "node_modules", + "**/node_modules/*" + ] +} \ No newline at end of file