Commit 33d656b8873320c7038b11a7908b0b1271253f02
0 parents
xfbh
Showing
29 changed files
with
2145 additions
and
0 deletions
.github/workflows/playwright.yml
0 → 100644
| 1 | +++ a/.github/workflows/playwright.yml | ||
| 1 | +name: Playwright Tests | ||
| 2 | +on: | ||
| 3 | + push: | ||
| 4 | + branches: [ main, master ] | ||
| 5 | + pull_request: | ||
| 6 | + branches: [ main, master ] | ||
| 7 | +jobs: | ||
| 8 | + test: | ||
| 9 | + timeout-minutes: 60 | ||
| 10 | + runs-on: ubuntu-latest | ||
| 11 | + steps: | ||
| 12 | + - uses: actions/checkout@v4 | ||
| 13 | + - uses: actions/setup-node@v4 | ||
| 14 | + with: | ||
| 15 | + node-version: lts/* | ||
| 16 | + - name: Install dependencies | ||
| 17 | + run: npm ci | ||
| 18 | + - name: Install Playwright Browsers | ||
| 19 | + run: npx playwright install --with-deps | ||
| 20 | + - name: Run Playwright tests | ||
| 21 | + run: npx playwright test | ||
| 22 | + - uses: actions/upload-artifact@v4 | ||
| 23 | + if: ${{ !cancelled() }} | ||
| 24 | + with: | ||
| 25 | + name: playwright-report | ||
| 26 | + path: playwright-report/ | ||
| 27 | + retention-days: 30 |
.gitignore
0 → 100644
REAADME.md
0 → 100644
| 1 | +++ a/REAADME.md | ||
| 1 | +# 项目名称:AI智能UI测试框架 | ||
| 2 | + | ||
| 3 | +## 快速开始 | ||
| 4 | +1. 安装依赖:`npm install` | ||
| 5 | +2. 复制环境变量:`cp .env.example .env` 并填写真实值 | ||
| 6 | +3. 运行测试:`npm test` | ||
| 7 | +4. 查看报告:`npm run report` | ||
| 8 | + | ||
| 9 | +## 添加新测试 | ||
| 10 | +1. 在 `pages/` 里创建页面对象 | ||
| 11 | +2. 在 `tests/` 里编写测试用例 | ||
| 12 | +3. 如果需要测试数据,放到 `data/` 里 | ||
| 13 | + | ||
| 14 | +## AI功能说明 | ||
| 15 | +- 视觉对比:使用 `expect(page).toHaveScreenshot()`,当UI变化时会提示差异。 | ||
| 16 | +- 自愈合定位:Playwright会自动尝试多种定位策略,减少维护成本。 | ||
| 0 | \ No newline at end of file | 17 | \ No newline at end of file |
ai/errorAnalyzer.ts
0 → 100644
| 1 | +++ a/ai/errorAnalyzer.ts |
ai/testGenerator.ts
0 → 100644
| 1 | +++ a/ai/testGenerator.ts |
e2e/example.spec.ts
0 → 100644
| 1 | +++ a/e2e/example.spec.ts | ||
| 1 | +import { test, expect } from '@playwright/test'; | ||
| 2 | + | ||
| 3 | +test('has title', async ({ page }) => { | ||
| 4 | + await page.goto('https://playwright.dev/'); | ||
| 5 | + | ||
| 6 | + // Expect a title "to contain" a substring. | ||
| 7 | + await expect(page).toHaveTitle(/Playwright/); | ||
| 8 | +}); | ||
| 9 | + | ||
| 10 | +test('get started link', async ({ page }) => { | ||
| 11 | + await page.goto('https://playwright.dev/'); | ||
| 12 | + | ||
| 13 | + // Click the get started link. | ||
| 14 | + await page.getByRole('link', { name: 'Get started' }).click(); | ||
| 15 | + | ||
| 16 | + // Expects page to have a heading with the name of Installation. | ||
| 17 | + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); | ||
| 18 | +}); |
fixtures/authFixture.ts
0 → 100644
| 1 | +++ a/fixtures/authFixture.ts | ||
| 1 | +import { test as base, Page } from '@playwright/test'; | ||
| 2 | +import path from 'path'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 认证夹具类型定义 | ||
| 6 | + */ | ||
| 7 | +type AuthFixtures = { | ||
| 8 | + /** | ||
| 9 | + * 已认证的页面(使用保存的 auth.json) | ||
| 10 | + */ | ||
| 11 | + authenticatedPage: Page; | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 认证文件路径 | ||
| 15 | + */ | ||
| 16 | + authFilePath: string; | ||
| 17 | +}; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * 扩展的测试对象,包含认证夹具 | ||
| 21 | + */ | ||
| 22 | +export const test = base.extend<AuthFixtures>({ | ||
| 23 | + // 默认认证文件路径 | ||
| 24 | + authFilePath: async ({}, use) => { | ||
| 25 | + await use(path.join(process.cwd(), 'auth.json')); | ||
| 26 | + }, | ||
| 27 | + | ||
| 28 | + // 已认证的页面 | ||
| 29 | + authenticatedPage: async ({ browser, authFilePath }, use) => { | ||
| 30 | + // 创建带有认证状态的上下文 | ||
| 31 | + const context = await browser.newContext({ | ||
| 32 | + storageState: authFilePath, | ||
| 33 | + }); | ||
| 34 | + | ||
| 35 | + const page = await context.newPage(); | ||
| 36 | + | ||
| 37 | + // 使用页面 | ||
| 38 | + await use(page); | ||
| 39 | + | ||
| 40 | + // 清理 | ||
| 41 | + await context.close(); | ||
| 42 | + }, | ||
| 43 | +}); | ||
| 44 | + | ||
| 45 | +/** | ||
| 46 | + * 导出 expect 以便在测试中使用 | ||
| 47 | + */ | ||
| 48 | +export { expect } from '@playwright/test'; | ||
| 49 | + | ||
| 50 | +/** | ||
| 51 | + * 创建一个新的认证夹具,使用自定义认证文件路径 | ||
| 52 | + * @param authPath 认证文件路径 | ||
| 53 | + */ | ||
| 54 | +export function createAuthTest(authPath: string) { | ||
| 55 | + return base.extend<AuthFixtures>({ | ||
| 56 | + authFilePath: async ({}, use) => { | ||
| 57 | + await use(authPath); | ||
| 58 | + }, | ||
| 59 | + authenticatedPage: async ({ browser, authFilePath }, use) => { | ||
| 60 | + const context = await browser.newContext({ | ||
| 61 | + storageState: authFilePath, | ||
| 62 | + }); | ||
| 63 | + const page = await context.newPage(); | ||
| 64 | + await use(page); | ||
| 65 | + await context.close(); | ||
| 66 | + }, | ||
| 67 | + }); | ||
| 68 | +} | ||
| 0 | \ No newline at end of file | 69 | \ No newline at end of file |
fixtures/index.ts
0 → 100644
| 1 | +++ a/fixtures/index.ts | ||
| 1 | +/** | ||
| 2 | + * 测试夹具统一导出入口 | ||
| 3 | + * | ||
| 4 | + * 使用方式: | ||
| 5 | + * 1. 普通测试(需要手动设置认证): | ||
| 6 | + * import { test, expect } from '../fixtures'; | ||
| 7 | + * | ||
| 8 | + * 2. 已认证测试(自动加载 auth.json): | ||
| 9 | + * import { fullTest as test, expect } from '../fixtures'; | ||
| 10 | + */ | ||
| 11 | + | ||
| 12 | +// 页面对象夹具 | ||
| 13 | +export { test, expect } from './testFixture'; | ||
| 14 | + | ||
| 15 | +// 认证夹具 | ||
| 16 | +export { test as authTest, expect as authExpect, createAuthTest } from './authFixture'; | ||
| 17 | + | ||
| 18 | +// 完整夹具(包含认证 + 页面对象) | ||
| 19 | +export { fullTest } from './testFixture'; | ||
| 20 | + | ||
| 21 | +// 类型导出 | ||
| 22 | +export type { PageFixtures } from './testFixture'; | ||
| 0 | \ No newline at end of file | 23 | \ No newline at end of file |
fixtures/testFixture.ts
0 → 100644
| 1 | +++ a/fixtures/testFixture.ts | ||
| 1 | +import { test as base, Page } from '@playwright/test'; | ||
| 2 | +import { LoginPage } from '../pages/loginPage'; | ||
| 3 | +import { ProductPage } from '../pages/productPage'; | ||
| 4 | +import { SalePage } from '../pages/salePage'; | ||
| 5 | +import { ConsignmentPage } from '../pages/consignmentPage'; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 页面对象夹具类型定义 | ||
| 9 | + */ | ||
| 10 | +export type PageFixtures = { | ||
| 11 | + /** | ||
| 12 | + * 登录页面 | ||
| 13 | + */ | ||
| 14 | + loginPage: LoginPage; | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 商品管理页面 | ||
| 18 | + */ | ||
| 19 | + productPage: ProductPage; | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 销售开单页面 | ||
| 23 | + */ | ||
| 24 | + salePage: SalePage; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 代销入库页面 | ||
| 28 | + */ | ||
| 29 | + consignmentPage: ConsignmentPage; | ||
| 30 | +}; | ||
| 31 | + | ||
| 32 | +/** | ||
| 33 | + * 扩展的测试对象,包含所有页面对象夹具 | ||
| 34 | + */ | ||
| 35 | +export const test = base.extend<PageFixtures>({ | ||
| 36 | + // 登录页面 | ||
| 37 | + loginPage: async ({ page }, use) => { | ||
| 38 | + await use(new LoginPage(page)); | ||
| 39 | + }, | ||
| 40 | + | ||
| 41 | + // 商品管理页面 | ||
| 42 | + productPage: async ({ page }, use) => { | ||
| 43 | + await use(new ProductPage(page)); | ||
| 44 | + }, | ||
| 45 | + | ||
| 46 | + // 销售开单页面 | ||
| 47 | + salePage: async ({ page }, use) => { | ||
| 48 | + await use(new SalePage(page)); | ||
| 49 | + }, | ||
| 50 | + | ||
| 51 | + // 代销入库页面 | ||
| 52 | + consignmentPage: async ({ page }, use) => { | ||
| 53 | + await use(new ConsignmentPage(page)); | ||
| 54 | + }, | ||
| 55 | +}); | ||
| 56 | + | ||
| 57 | +/** | ||
| 58 | + * 导出 expect 以便在测试中使用 | ||
| 59 | + */ | ||
| 60 | +export { expect } from '@playwright/test'; | ||
| 61 | + | ||
| 62 | +/** | ||
| 63 | + * 创建一个同时包含认证和页面对象的完整测试夹具 | ||
| 64 | + */ | ||
| 65 | +import { test as authTest } from './authFixture'; | ||
| 66 | + | ||
| 67 | +type FullFixtures = PageFixtures & { | ||
| 68 | + authenticatedPage: Page; | ||
| 69 | + authFilePath: string; | ||
| 70 | +}; | ||
| 71 | + | ||
| 72 | +export const fullTest = authTest.extend<PageFixtures>({ | ||
| 73 | + loginPage: async ({ authenticatedPage }, use) => { | ||
| 74 | + await use(new LoginPage(authenticatedPage)); | ||
| 75 | + }, | ||
| 76 | + | ||
| 77 | + productPage: async ({ authenticatedPage }, use) => { | ||
| 78 | + await use(new ProductPage(authenticatedPage)); | ||
| 79 | + }, | ||
| 80 | + | ||
| 81 | + salePage: async ({ authenticatedPage }, use) => { | ||
| 82 | + await use(new SalePage(authenticatedPage)); | ||
| 83 | + }, | ||
| 84 | + | ||
| 85 | + consignmentPage: async ({ authenticatedPage }, use) => { | ||
| 86 | + await use(new ConsignmentPage(authenticatedPage)); | ||
| 87 | + }, | ||
| 88 | +}); | ||
| 0 | \ No newline at end of file | 89 | \ No newline at end of file |
package-lock.json
0 → 100644
| 1 | +++ a/package-lock.json | ||
| 1 | +{ | ||
| 2 | + "name": "xfbh", | ||
| 3 | + "version": "1.0.0", | ||
| 4 | + "lockfileVersion": 3, | ||
| 5 | + "requires": true, | ||
| 6 | + "packages": { | ||
| 7 | + "": { | ||
| 8 | + "name": "xfbh", | ||
| 9 | + "version": "1.0.0", | ||
| 10 | + "license": "ISC", | ||
| 11 | + "devDependencies": { | ||
| 12 | + "@playwright/test": "^1.58.2", | ||
| 13 | + "@types/node": "^25.3.1", | ||
| 14 | + "allure-commandline": "^2.37.0", | ||
| 15 | + "allure-playwright": "^3.5.0", | ||
| 16 | + "dotenv": "^17.3.1", | ||
| 17 | + "typescript": "^5.9.3" | ||
| 18 | + } | ||
| 19 | + }, | ||
| 20 | + "node_modules/@playwright/test": { | ||
| 21 | + "version": "1.58.2", | ||
| 22 | + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", | ||
| 23 | + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", | ||
| 24 | + "dev": true, | ||
| 25 | + "license": "Apache-2.0", | ||
| 26 | + "dependencies": { | ||
| 27 | + "playwright": "1.58.2" | ||
| 28 | + }, | ||
| 29 | + "bin": { | ||
| 30 | + "playwright": "cli.js" | ||
| 31 | + }, | ||
| 32 | + "engines": { | ||
| 33 | + "node": ">=18" | ||
| 34 | + } | ||
| 35 | + }, | ||
| 36 | + "node_modules/@types/node": { | ||
| 37 | + "version": "25.3.1", | ||
| 38 | + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.1.tgz", | ||
| 39 | + "integrity": "sha512-hj9YIJimBCipHVfHKRMnvmHg+wfhKc0o4mTtXh9pKBjC8TLJzz0nzGmLi5UJsYAUgSvXFHgb0V2oY10DUFtImw==", | ||
| 40 | + "dev": true, | ||
| 41 | + "license": "MIT", | ||
| 42 | + "dependencies": { | ||
| 43 | + "undici-types": "~7.18.0" | ||
| 44 | + } | ||
| 45 | + }, | ||
| 46 | + "node_modules/allure-commandline": { | ||
| 47 | + "version": "2.37.0", | ||
| 48 | + "resolved": "https://registry.npmjs.org/allure-commandline/-/allure-commandline-2.37.0.tgz", | ||
| 49 | + "integrity": "sha512-s3zZ8zjqo2U3i5Lb3iLOCjwWQCtGK58GVpScTnZddOpgTXBDXAbXn+pT7QXN4NiY7pho6xw+UgyREyCRnx/9ug==", | ||
| 50 | + "dev": true, | ||
| 51 | + "license": "Apache-2.0", | ||
| 52 | + "bin": { | ||
| 53 | + "allure": "bin/allure" | ||
| 54 | + } | ||
| 55 | + }, | ||
| 56 | + "node_modules/allure-js-commons": { | ||
| 57 | + "version": "3.5.0", | ||
| 58 | + "resolved": "https://registry.npmjs.org/allure-js-commons/-/allure-js-commons-3.5.0.tgz", | ||
| 59 | + "integrity": "sha512-iBVFNQkX5i48QGlb5U3iWm+NiNOl/ucxv6dvEJBNeJTPMI8t0Dn0CuXMQEiv4forSSAppD7FB9uGal2JwunH/A==", | ||
| 60 | + "dev": true, | ||
| 61 | + "license": "Apache-2.0", | ||
| 62 | + "dependencies": { | ||
| 63 | + "md5": "^2.3.0" | ||
| 64 | + }, | ||
| 65 | + "peerDependencies": { | ||
| 66 | + "allure-playwright": "3.5.0" | ||
| 67 | + }, | ||
| 68 | + "peerDependenciesMeta": { | ||
| 69 | + "allure-playwright": { | ||
| 70 | + "optional": true | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + }, | ||
| 74 | + "node_modules/allure-playwright": { | ||
| 75 | + "version": "3.5.0", | ||
| 76 | + "resolved": "https://registry.npmjs.org/allure-playwright/-/allure-playwright-3.5.0.tgz", | ||
| 77 | + "integrity": "sha512-nB6Wj1z7oGz44r4qxN2lJ6lgDQ+FcpL2dyhUsH/syyNPY8x1JLandedc3FA+nqtxoer6qUagsWZfDZnsDO0RXA==", | ||
| 78 | + "dev": true, | ||
| 79 | + "license": "Apache-2.0", | ||
| 80 | + "dependencies": { | ||
| 81 | + "allure-js-commons": "3.5.0" | ||
| 82 | + }, | ||
| 83 | + "peerDependencies": { | ||
| 84 | + "@playwright/test": ">=1.53.0" | ||
| 85 | + } | ||
| 86 | + }, | ||
| 87 | + "node_modules/charenc": { | ||
| 88 | + "version": "0.0.2", | ||
| 89 | + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", | ||
| 90 | + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", | ||
| 91 | + "dev": true, | ||
| 92 | + "license": "BSD-3-Clause", | ||
| 93 | + "engines": { | ||
| 94 | + "node": "*" | ||
| 95 | + } | ||
| 96 | + }, | ||
| 97 | + "node_modules/crypt": { | ||
| 98 | + "version": "0.0.2", | ||
| 99 | + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", | ||
| 100 | + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", | ||
| 101 | + "dev": true, | ||
| 102 | + "license": "BSD-3-Clause", | ||
| 103 | + "engines": { | ||
| 104 | + "node": "*" | ||
| 105 | + } | ||
| 106 | + }, | ||
| 107 | + "node_modules/dotenv": { | ||
| 108 | + "version": "17.3.1", | ||
| 109 | + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", | ||
| 110 | + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", | ||
| 111 | + "dev": true, | ||
| 112 | + "license": "BSD-2-Clause", | ||
| 113 | + "engines": { | ||
| 114 | + "node": ">=12" | ||
| 115 | + }, | ||
| 116 | + "funding": { | ||
| 117 | + "url": "https://dotenvx.com" | ||
| 118 | + } | ||
| 119 | + }, | ||
| 120 | + "node_modules/fsevents": { | ||
| 121 | + "version": "2.3.2", | ||
| 122 | + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", | ||
| 123 | + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", | ||
| 124 | + "dev": true, | ||
| 125 | + "hasInstallScript": true, | ||
| 126 | + "license": "MIT", | ||
| 127 | + "optional": true, | ||
| 128 | + "os": [ | ||
| 129 | + "darwin" | ||
| 130 | + ], | ||
| 131 | + "engines": { | ||
| 132 | + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" | ||
| 133 | + } | ||
| 134 | + }, | ||
| 135 | + "node_modules/is-buffer": { | ||
| 136 | + "version": "1.1.6", | ||
| 137 | + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", | ||
| 138 | + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", | ||
| 139 | + "dev": true, | ||
| 140 | + "license": "MIT" | ||
| 141 | + }, | ||
| 142 | + "node_modules/md5": { | ||
| 143 | + "version": "2.3.0", | ||
| 144 | + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", | ||
| 145 | + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", | ||
| 146 | + "dev": true, | ||
| 147 | + "license": "BSD-3-Clause", | ||
| 148 | + "dependencies": { | ||
| 149 | + "charenc": "0.0.2", | ||
| 150 | + "crypt": "0.0.2", | ||
| 151 | + "is-buffer": "~1.1.6" | ||
| 152 | + } | ||
| 153 | + }, | ||
| 154 | + "node_modules/playwright": { | ||
| 155 | + "version": "1.58.2", | ||
| 156 | + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", | ||
| 157 | + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", | ||
| 158 | + "dev": true, | ||
| 159 | + "license": "Apache-2.0", | ||
| 160 | + "dependencies": { | ||
| 161 | + "playwright-core": "1.58.2" | ||
| 162 | + }, | ||
| 163 | + "bin": { | ||
| 164 | + "playwright": "cli.js" | ||
| 165 | + }, | ||
| 166 | + "engines": { | ||
| 167 | + "node": ">=18" | ||
| 168 | + }, | ||
| 169 | + "optionalDependencies": { | ||
| 170 | + "fsevents": "2.3.2" | ||
| 171 | + } | ||
| 172 | + }, | ||
| 173 | + "node_modules/playwright-core": { | ||
| 174 | + "version": "1.58.2", | ||
| 175 | + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", | ||
| 176 | + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", | ||
| 177 | + "dev": true, | ||
| 178 | + "license": "Apache-2.0", | ||
| 179 | + "bin": { | ||
| 180 | + "playwright-core": "cli.js" | ||
| 181 | + }, | ||
| 182 | + "engines": { | ||
| 183 | + "node": ">=18" | ||
| 184 | + } | ||
| 185 | + }, | ||
| 186 | + "node_modules/typescript": { | ||
| 187 | + "version": "5.9.3", | ||
| 188 | + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", | ||
| 189 | + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", | ||
| 190 | + "dev": true, | ||
| 191 | + "license": "Apache-2.0", | ||
| 192 | + "bin": { | ||
| 193 | + "tsc": "bin/tsc", | ||
| 194 | + "tsserver": "bin/tsserver" | ||
| 195 | + }, | ||
| 196 | + "engines": { | ||
| 197 | + "node": ">=14.17" | ||
| 198 | + } | ||
| 199 | + }, | ||
| 200 | + "node_modules/undici-types": { | ||
| 201 | + "version": "7.18.2", | ||
| 202 | + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", | ||
| 203 | + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", | ||
| 204 | + "dev": true, | ||
| 205 | + "license": "MIT" | ||
| 206 | + } | ||
| 207 | + } | ||
| 208 | +} |
package.json
0 → 100644
| 1 | +++ a/package.json | ||
| 1 | +{ | ||
| 2 | + "name": "xfbh", | ||
| 3 | + "version": "1.0.0", | ||
| 4 | + "main": "index.js", | ||
| 5 | + "scripts": { | ||
| 6 | + "test:ci": "npx playwright test", | ||
| 7 | + "save-auth": "scripts/save-auth.ts" | ||
| 8 | + }, | ||
| 9 | + "keywords": [], | ||
| 10 | + "author": "", | ||
| 11 | + "license": "ISC", | ||
| 12 | + "description": "", | ||
| 13 | + "devDependencies": { | ||
| 14 | + "@playwright/test": "^1.58.2", | ||
| 15 | + "@types/node": "^25.3.1", | ||
| 16 | + "allure-commandline": "^2.37.0", | ||
| 17 | + "allure-playwright": "^3.5.0", | ||
| 18 | + "dotenv": "^17.3.1", | ||
| 19 | + "typescript": "^5.9.3" | ||
| 20 | + } | ||
| 21 | +} |
pages/basePage.ts
0 → 100644
| 1 | +++ a/pages/basePage.ts | ||
| 1 | +import { Page, Locator, expect } from '@playwright/test'; | ||
| 2 | +import {test} from '@playwright/test'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 基础页面类 - 所有页面对象的父类 | ||
| 6 | + * 提供通用的页面操作方法 | ||
| 7 | + */ | ||
| 8 | + | ||
| 9 | +export abstract class BasePage { | ||
| 10 | + readonly page: Page; | ||
| 11 | + | ||
| 12 | + constructor(page: Page) { | ||
| 13 | + this.page = page; | ||
| 14 | + } | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + 截图并附加到Allure报告 | ||
| 18 | + @param testName 测试名称 | ||
| 19 | + @param screenshotName 截图名称 | ||
| 20 | + */ | ||
| 21 | + async attachScreenshot(testInfo:any,screenshotName:string): | ||
| 22 | + Promise<void>{ | ||
| 23 | + const screenshot = await this.page.screenshot(); | ||
| 24 | + await testInfo.attach(screenshotName,{ | ||
| 25 | + body:screenshot, | ||
| 26 | + contentType:'image/png' | ||
| 27 | + }); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 导航到指定路径 | ||
| 32 | + * @param path 相对路径 | ||
| 33 | + */ | ||
| 34 | + async navigate(path: string = '/'): Promise<void> { | ||
| 35 | + const baseURL = process.env.BASE_URL || 'https://erp-pad.test.gszdtop.com'; | ||
| 36 | + await this.page.goto(`${baseURL}/#${path}`); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * 等待页面加载完成 | ||
| 41 | + */ | ||
| 42 | + async waitForPageLoad(): Promise<void> { | ||
| 43 | + await this.page.waitForLoadState('networkidle'); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 点击元素 | ||
| 48 | + * @param selector 选择器或定位器 | ||
| 49 | + */ | ||
| 50 | + async click(selector: string | Locator): Promise<void> { | ||
| 51 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 52 | + await locator.click(); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 点击包含指定文本的元素 | ||
| 57 | + * @param text 文本内容 | ||
| 58 | + */ | ||
| 59 | + async clickByText(text: string): Promise<void> { | ||
| 60 | + await this.page.getByText(text).click(); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 填写输入框 | ||
| 65 | + * @param selector 选择器或定位器 | ||
| 66 | + * @param value 填写的值 | ||
| 67 | + */ | ||
| 68 | + async fill(selector: string | Locator, value: string): Promise<void> { | ||
| 69 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 70 | + await locator.fill(value); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 清空并填写输入框 | ||
| 75 | + * @param selector 选择器或定位器 | ||
| 76 | + * @param value 填写的值 | ||
| 77 | + */ | ||
| 78 | + async clearAndFill(selector: string | Locator, value: string): Promise<void> { | ||
| 79 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 80 | + await locator.clear(); | ||
| 81 | + await locator.fill(value); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 等待元素可见 | ||
| 86 | + * @param selector 选择器或定位器 | ||
| 87 | + * @param timeout 超时时间(毫秒) | ||
| 88 | + */ | ||
| 89 | + async waitForVisible(selector: string | Locator, timeout: number = 30000): Promise<void> { | ||
| 90 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 91 | + await locator.waitFor({ state: 'visible', timeout }); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + /** | ||
| 95 | + * 等待元素附加到 DOM | ||
| 96 | + * @param selector 选择器或定位器 | ||
| 97 | + * @param timeout 超时时间(毫秒) | ||
| 98 | + */ | ||
| 99 | + async waitForAttached(selector: string | Locator, timeout: number = 30000): Promise<void> { | ||
| 100 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 101 | + await locator.waitFor({ state: 'attached', timeout }); | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 等待元素隐藏 | ||
| 106 | + * @param selector 选择器或定位器 | ||
| 107 | + * @param timeout 超时时间(毫秒) | ||
| 108 | + */ | ||
| 109 | + async waitForHidden(selector: string | Locator, timeout: number = 30000): Promise<void> { | ||
| 110 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 111 | + await locator.waitFor({ state: 'hidden', timeout }); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + /** | ||
| 115 | + * 断言元素可见 | ||
| 116 | + * @param selector 选择器或定位器 | ||
| 117 | + */ | ||
| 118 | + async expectVisible(selector: string | Locator): Promise<void> { | ||
| 119 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 120 | + await expect(locator).toBeVisible(); | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + /** | ||
| 124 | + * 断言元素包含指定文本 | ||
| 125 | + * @param selector 选择器或定位器 | ||
| 126 | + * @param text 期望的文本 | ||
| 127 | + */ | ||
| 128 | + async expectText(selector: string | Locator, text: string | RegExp): Promise<void> { | ||
| 129 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 130 | + await expect(locator).toContainText(text); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + /** | ||
| 134 | + * 断言元素具有确切文本 | ||
| 135 | + * @param selector 选择器或定位器 | ||
| 136 | + * @param text 期望的确切文本 | ||
| 137 | + */ | ||
| 138 | + async expectExactText(selector: string | Locator, text: string | RegExp): Promise<void> { | ||
| 139 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 140 | + await expect(locator).toHaveText(text); | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + /** | ||
| 144 | + * 选择下拉列表选项(通过文本匹配) | ||
| 145 | + * @param selector 选择器 | ||
| 146 | + * @param optionText 选项文本 | ||
| 147 | + */ | ||
| 148 | + async selectOptionByText(selector: string, optionText: string): Promise<void> { | ||
| 149 | + await this.page.locator(selector).selectOption({ label: optionText }); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + /** | ||
| 153 | + * 获取元素文本内容 | ||
| 154 | + * @param selector 选择器或定位器 | ||
| 155 | + */ | ||
| 156 | + async getText(selector: string | Locator): Promise<string | null> { | ||
| 157 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 158 | + return locator.textContent(); | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + /** | ||
| 162 | + * 获取输入框的值 | ||
| 163 | + * @param selector 选择器或定位器 | ||
| 164 | + */ | ||
| 165 | + async getInputValue(selector: string | Locator): Promise<string> { | ||
| 166 | + const locator = typeof selector === 'string' ? this.page.locator(selector) : selector; | ||
| 167 | + return locator.inputValue(); | ||
| 168 | + } | ||
| 169 | + | ||
| 170 | + /** | ||
| 171 | + * 截图 | ||
| 172 | + * @param name 截图名称 | ||
| 173 | + */ | ||
| 174 | + async takeScreenshot(name: string): Promise<Buffer> { | ||
| 175 | + return this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true }); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + /** | ||
| 179 | + * 等待指定时间 | ||
| 180 | + * @param ms 毫秒数 | ||
| 181 | + */ | ||
| 182 | + async wait(ms: number): Promise<void> { | ||
| 183 | + await this.page.waitForTimeout(ms); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + /** | ||
| 187 | + * 获取页面 URL | ||
| 188 | + */ | ||
| 189 | + getUrl(): string { | ||
| 190 | + return this.page.url(); | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + /** | ||
| 194 | + * 刷新页面 | ||
| 195 | + */ | ||
| 196 | + async reload(): Promise<void> { | ||
| 197 | + await this.page.reload({ waitUntil: 'networkidle' }); | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + /** | ||
| 201 | + * 等待导航完成 | ||
| 202 | + * @param timeout 超时时间 | ||
| 203 | + */ | ||
| 204 | + async waitForNavigation(timeout: number = 30000): Promise<void> { | ||
| 205 | + await this.page.waitForLoadState('load', { timeout }); | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + /** | ||
| 209 | + * 获取定位器 | ||
| 210 | + * @param selector 选择器 | ||
| 211 | + */ | ||
| 212 | + getLocator(selector: string): Locator { | ||
| 213 | + return this.page.locator(selector); | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + /** | ||
| 217 | + * 过滤定位器 | ||
| 218 | + * @param selector 选择器 | ||
| 219 | + * @param options 过滤选项 | ||
| 220 | + */ | ||
| 221 | + filterLocator(selector: string, options: { hasText?: string | RegExp; has?: Locator }): Locator { | ||
| 222 | + return this.page.locator(selector).filter(options); | ||
| 223 | + } | ||
| 224 | +} |
pages/consignmentPage.ts
0 → 100644
| 1 | +++ a/pages/consignmentPage.ts | ||
| 1 | +import { Page, Locator, expect } from '@playwright/test'; | ||
| 2 | +import { BasePage } from './basePage'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 代销入库页面选择器 | ||
| 6 | + */ | ||
| 7 | +const selectors = { | ||
| 8 | + // 导航 | ||
| 9 | + moreMenu: 'text=更多', | ||
| 10 | + consignmentInMenu: 'text=代销入库', | ||
| 11 | + addConsignmentButton: 'text=新增代卖入库', | ||
| 12 | + | ||
| 13 | + // 仓库选择 | ||
| 14 | + warehouseSelector: '.nut-input__mask', | ||
| 15 | + warehouseOption: (warehouseName: string) => `uni-view:has-text("${warehouseName}")`, | ||
| 16 | + mainTitleOption: '.mainTitle', | ||
| 17 | + | ||
| 18 | + // 批次别名输入 | ||
| 19 | + batchAliasInput: 'uni-input:has-text("请输入批次别名") input', | ||
| 20 | + | ||
| 21 | + // 商品选择 | ||
| 22 | + productListButton: 'text=商品清单点击选择商品', | ||
| 23 | + productOption: (productName: string) => `text=${productName}`, | ||
| 24 | + numberKey: (key: string) => `text=${key}`, | ||
| 25 | + | ||
| 26 | + // 费用 | ||
| 27 | + addExpenseButton: 'text=入库费用添加费用', | ||
| 28 | + expenseItem: '.box-item', | ||
| 29 | + amountInput: '[role="spinbutton"]', | ||
| 30 | + paymentItem: '.payment-item', | ||
| 31 | + | ||
| 32 | + // 操作 | ||
| 33 | + createButton: 'text=创建', | ||
| 34 | + saveButton: 'text=保存', | ||
| 35 | + doneButton:'text=完成', | ||
| 36 | + | ||
| 37 | + // 验证 | ||
| 38 | + batchNameInList: (batchName: string) => `uni-scroll-view:has-text("${batchName}")`, | ||
| 39 | +}; | ||
| 40 | + | ||
| 41 | +/** | ||
| 42 | + * 代销入库页面类 | ||
| 43 | + * 处理代销入库相关操作 | ||
| 44 | + */ | ||
| 45 | +export class ConsignmentPage extends BasePage { | ||
| 46 | + // 导航定位器 | ||
| 47 | + readonly moreMenu: Locator; | ||
| 48 | + readonly consignmentInMenu: Locator; | ||
| 49 | + readonly addConsignmentButton: Locator; | ||
| 50 | + | ||
| 51 | + // 仓库选择 | ||
| 52 | + readonly warehouseSelector: Locator; | ||
| 53 | + readonly mainTitleOption: Locator; | ||
| 54 | + | ||
| 55 | + // 批次别名 | ||
| 56 | + readonly batchAliasInput: Locator; | ||
| 57 | + | ||
| 58 | + // 商品选择 | ||
| 59 | + readonly productListButton: Locator; | ||
| 60 | + | ||
| 61 | + // 费用 | ||
| 62 | + readonly addExpenseButton: Locator; | ||
| 63 | + readonly expenseItem: Locator; | ||
| 64 | + readonly amountInput: Locator; | ||
| 65 | + readonly paymentItem: Locator; | ||
| 66 | + | ||
| 67 | + // 操作按钮 | ||
| 68 | + readonly createButton: Locator; | ||
| 69 | + readonly saveButton: Locator; | ||
| 70 | + readonly doneButton:Locator; | ||
| 71 | + | ||
| 72 | + constructor(page: Page) { | ||
| 73 | + super(page); | ||
| 74 | + | ||
| 75 | + this.moreMenu = page.getByText('更多'); | ||
| 76 | + this.consignmentInMenu = page.getByText('代销入库').first(); | ||
| 77 | + this.addConsignmentButton = page.getByText('新增代卖入库'); | ||
| 78 | + this.warehouseSelector = page.locator('.nut-input__mask').first(); | ||
| 79 | + this.mainTitleOption = page.locator('.mainTitle').first(); | ||
| 80 | + this.batchAliasInput = page.locator('uni-input').filter({ hasText: '请输入批次别名' }).getByRole('textbox'); | ||
| 81 | + this.productListButton = page.getByText('商品清单点击选择商品'); | ||
| 82 | + this.addExpenseButton = page.getByText('入库费用添加费用'); | ||
| 83 | + this.expenseItem = page.locator('.box-item'); | ||
| 84 | + this.amountInput = page.getByRole('spinbutton'); | ||
| 85 | + this.paymentItem = page.locator('.payment-item'); | ||
| 86 | + this.createButton = page.getByText('创建', { exact: true }); | ||
| 87 | + this.saveButton = page.getByText('保存'); | ||
| 88 | + this.doneButton = page.getByText('完成') | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + /** | ||
| 92 | + * 导航到首页并等待加载 | ||
| 93 | + */ | ||
| 94 | + async gotoHome(): Promise<void> { | ||
| 95 | + await this.navigate('/'); | ||
| 96 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + /** | ||
| 100 | + * 打开更多菜单 | ||
| 101 | + */ | ||
| 102 | + async openMoreMenu(): Promise<void> { | ||
| 103 | + await this.moreMenu.click(); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + /** | ||
| 107 | + * 打开代销入库菜单 | ||
| 108 | + */ | ||
| 109 | + async openConsignmentIn(): Promise<void> { | ||
| 110 | + await this.consignmentInMenu.click(); | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + /** | ||
| 114 | + * 点击新增代卖入库 | ||
| 115 | + */ | ||
| 116 | + async clickAddConsignment(): Promise<void> { | ||
| 117 | + await this.addConsignmentButton.click(); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * 导航到新增代销入库页面(完整流程) | ||
| 122 | + */ | ||
| 123 | + async navigateToNewConsignment(): Promise<void> { | ||
| 124 | + await this.gotoHome(); | ||
| 125 | + await this.openMoreMenu(); | ||
| 126 | + await this.openConsignmentIn(); | ||
| 127 | + await this.clickAddConsignment(); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + /** | ||
| 131 | + * 选择仓库 | ||
| 132 | + * @param warehouseName 仓库名称(可选,默认选择第一个) | ||
| 133 | + */ | ||
| 134 | + async selectWarehouse(warehouseName?: string): Promise<void> { | ||
| 135 | + // 点击仓库选择器 | ||
| 136 | + await this.warehouseSelector.click(); | ||
| 137 | + | ||
| 138 | + if (warehouseName) { | ||
| 139 | + // 选择指定仓库 | ||
| 140 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${warehouseName}$`) }).nth(1).click(); | ||
| 141 | + } else { | ||
| 142 | + // 默认选择第一个 | ||
| 143 | + await this.mainTitleOption.click(); | ||
| 144 | + } | ||
| 145 | + } | ||
| 146 | + | ||
| 147 | + /** | ||
| 148 | + * 选择第二个仓库 | ||
| 149 | + */ | ||
| 150 | + async selectSecondWarehouse(): Promise<void> { | ||
| 151 | + await this.page.locator('uni-view:nth-child(4) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask').click(); | ||
| 152 | + await this.page.locator('uni-view').filter({ hasText: /^东区普通仓库$/ }).nth(1).click(); | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + /** | ||
| 156 | + * 输入批次别名 | ||
| 157 | + * @param alias 批次别名 | ||
| 158 | + */ | ||
| 159 | + async enterBatchAlias(alias: string): Promise<void> { | ||
| 160 | + await this.batchAliasInput.click(); | ||
| 161 | + await this.batchAliasInput.clear(); | ||
| 162 | + await this.batchAliasInput.fill(alias); | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + /** | ||
| 166 | + * 打开商品选择列表 | ||
| 167 | + */ | ||
| 168 | + async openProductList(): Promise<void> { | ||
| 169 | + await this.productListButton.click(); | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + /** | ||
| 173 | + * 选择商品 | ||
| 174 | + * @param productName 商品名称 | ||
| 175 | + */ | ||
| 176 | + async selectProduct(productName: string): Promise<void> { | ||
| 177 | + // 在商品选择弹窗中选择商品,使用first()避免多个匹配项导致的strict mode violation | ||
| 178 | + await this.page.locator('.productName').getByText(productName).click(); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + /** | ||
| 182 | + * 输入商品数量 | ||
| 183 | + * @param quantity 数量(数字字符串,如 "10") | ||
| 184 | + */ | ||
| 185 | + async enterProductQuantity(quantity: string): Promise<void> { | ||
| 186 | + for (const digit of quantity) { | ||
| 187 | + await this.page.getByText(digit, { exact: true }).click(); | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + /** | ||
| 192 | + * 完成商品选择 | ||
| 193 | + */ | ||
| 194 | + async completeProductSelection(): Promise<void> { | ||
| 195 | + await this.doneButton.click(); // 点击完成按钮(同保存按钮) | ||
| 196 | + await this.saveButton.click() | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + /** | ||
| 200 | + * 选择商品并输入数量 | ||
| 201 | + * @param productName 商品名称 | ||
| 202 | + * @param quantity 数量 | ||
| 203 | + */ | ||
| 204 | + async selectProductWithQuantity(productName: string, quantity: string): Promise<void> { | ||
| 205 | + await this.openProductList(); | ||
| 206 | + await this.selectProduct(productName); | ||
| 207 | + await this.enterProductQuantity(quantity); | ||
| 208 | + await this.completeProductSelection(); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + /** | ||
| 212 | + * 添加费用项 | ||
| 213 | + * @param expenseIndex 费用项索引(默认第一个) | ||
| 214 | + * @param amount 金额 | ||
| 215 | + */ | ||
| 216 | + async addExpense(expenseIndex: number = 0, amount: string = '1'): Promise<void> { | ||
| 217 | + // 打开费用选择 | ||
| 218 | + await this.addExpenseButton.click(); | ||
| 219 | + | ||
| 220 | + // 等待费用列表加载 | ||
| 221 | + await this.expenseItem.first().waitFor({ state: 'attached' }); | ||
| 222 | + | ||
| 223 | + // 选择费用项 | ||
| 224 | + await this.expenseItem.nth(expenseIndex).click(); | ||
| 225 | + await this.page.getByText('确定').click(); | ||
| 226 | + | ||
| 227 | + // 输入金额 | ||
| 228 | + await this.page.getByText('金额*0.00').click(); | ||
| 229 | + await this.amountInput.click(); | ||
| 230 | + await this.amountInput.fill(amount); | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + /** | ||
| 234 | + * 选择支付方式 | ||
| 235 | + * @param paymentIndex 支付方式索引(默认第一个) | ||
| 236 | + */ | ||
| 237 | + async selectPaymentMethod(paymentIndex: number = 0): Promise<void> { | ||
| 238 | + // 点击支付方式选择器 | ||
| 239 | + await this.page.locator('uni-view:nth-child(6) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask').click(); | ||
| 240 | + | ||
| 241 | + // 等待支付方式列表加载 | ||
| 242 | + await this.paymentItem.first().waitFor({ state: 'attached' }); | ||
| 243 | + | ||
| 244 | + // 选择支付方式 | ||
| 245 | + await this.paymentItem.nth(paymentIndex).click(); | ||
| 246 | + await this.page.getByText('确定').click(); | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + /** | ||
| 250 | + * 点击创建按钮 | ||
| 251 | + */ | ||
| 252 | + async clickCreate(): Promise<void> { | ||
| 253 | + await this.createButton.click(); | ||
| 254 | + } | ||
| 255 | + | ||
| 256 | + /** | ||
| 257 | + * 等待创建完成 | ||
| 258 | + */ | ||
| 259 | + async waitForCreationComplete(): Promise<void> { | ||
| 260 | + await this.page.waitForLoadState('networkidle'); | ||
| 261 | + await this.wait(1000); // 额外等待1秒,适配uni-app页面渲染延迟 | ||
| 262 | + } | ||
| 263 | + | ||
| 264 | + /** | ||
| 265 | + * 验证批次创建成功 | ||
| 266 | + * @param batchName 批次名称 | ||
| 267 | + */ | ||
| 268 | + async expectBatchCreated(batchName: string): Promise<void> { | ||
| 269 | + await expect(this.page.locator('uni-scroll-view').getByText(batchName)).toBeVisible(); | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + /** | ||
| 273 | + * 完整的代销入库创建流程 | ||
| 274 | + * @param batchAlias 批次别名 | ||
| 275 | + * @param productName 商品名称 | ||
| 276 | + * @param quantity 数量 | ||
| 277 | + * @param amount 费用金额 | ||
| 278 | + */ | ||
| 279 | + async createConsignmentOrder( | ||
| 280 | + batchAlias: string, | ||
| 281 | + productName: string, | ||
| 282 | + quantity: string = '10', | ||
| 283 | + amount: string = '1' | ||
| 284 | + ): Promise<void> { | ||
| 285 | + // 导航到新增页面 | ||
| 286 | + await this.navigateToNewConsignment(); | ||
| 287 | + | ||
| 288 | + // 选择仓库 | ||
| 289 | + await this.selectWarehouse(); | ||
| 290 | + await this.selectSecondWarehouse(); | ||
| 291 | + | ||
| 292 | + // 输入批次别名 | ||
| 293 | + await this.enterBatchAlias(batchAlias); | ||
| 294 | + | ||
| 295 | + // 选择商品 | ||
| 296 | + await this.selectProductWithQuantity(productName, quantity); | ||
| 297 | + | ||
| 298 | + // 添加费用 | ||
| 299 | + await this.addExpense(0, amount); | ||
| 300 | + await this.selectPaymentMethod(0); | ||
| 301 | + | ||
| 302 | + // 创建 | ||
| 303 | + await this.clickCreate(); | ||
| 304 | + await this.waitForCreationComplete(); | ||
| 305 | + | ||
| 306 | + // 验证 | ||
| 307 | + await this.expectBatchCreated(batchAlias); | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + /** | ||
| 311 | + * 简化的代销入库创建流程(不添加费用) | ||
| 312 | + * @param batchAlias 批次别名 | ||
| 313 | + * @param productName 商品名称 | ||
| 314 | + * @param quantity 数量 | ||
| 315 | + */ | ||
| 316 | + async createSimpleConsignmentOrder( | ||
| 317 | + batchAlias: string, | ||
| 318 | + productName: string, | ||
| 319 | + quantity: string = '10' | ||
| 320 | + ): Promise<void> { | ||
| 321 | + // 导航到新增页面 | ||
| 322 | + await this.navigateToNewConsignment(); | ||
| 323 | + | ||
| 324 | + // 选择仓库 | ||
| 325 | + await this.selectWarehouse(); | ||
| 326 | + await this.selectSecondWarehouse(); | ||
| 327 | + | ||
| 328 | + // 输入批次别名 | ||
| 329 | + await this.enterBatchAlias(batchAlias); | ||
| 330 | + | ||
| 331 | + // 选择商品 | ||
| 332 | + await this.selectProductWithQuantity(productName, quantity); | ||
| 333 | + | ||
| 334 | + // 创建 | ||
| 335 | + await this.clickCreate(); | ||
| 336 | + await this.waitForCreationComplete(); | ||
| 337 | + } | ||
| 338 | +} | ||
| 0 | \ No newline at end of file | 339 | \ No newline at end of file |
pages/loginPage.ts
0 → 100644
| 1 | +++ a/pages/loginPage.ts | ||
| 1 | +import { Page, Locator, expect } from '@playwright/test'; | ||
| 2 | +import { BasePage } from './basePage'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 登录页面选择器 | ||
| 6 | + */ | ||
| 7 | +const selectors = { | ||
| 8 | + phoneLoginTab: 'text=手机号登录/注册', | ||
| 9 | + confirmButton: 'text=确定', | ||
| 10 | + phoneInput: 'uni-input:has-text("请输入手机号") input', | ||
| 11 | + getCodeButton: 'uni-button:has-text("获取验证码")', | ||
| 12 | + loginButton: 'uni-button:has-text("登录")', | ||
| 13 | + userInfo: (userName: string) => `text=${userName}`, | ||
| 14 | +}; | ||
| 15 | + | ||
| 16 | +/** | ||
| 17 | + * 登录页面类 | ||
| 18 | + * 处理用户登录相关的操作 | ||
| 19 | + */ | ||
| 20 | +export class LoginPage extends BasePage { | ||
| 21 | + // 定位器 | ||
| 22 | + readonly phoneLoginTab: Locator; | ||
| 23 | + readonly confirmButton: Locator; | ||
| 24 | + readonly phoneInput: Locator; | ||
| 25 | + readonly getCodeButton: Locator; | ||
| 26 | + readonly loginButton: Locator; | ||
| 27 | + | ||
| 28 | + constructor(page: Page) { | ||
| 29 | + super(page); | ||
| 30 | + | ||
| 31 | + this.phoneLoginTab = page.getByText('手机号登录/注册'); | ||
| 32 | + this.confirmButton = page.getByText('确定'); | ||
| 33 | + this.phoneInput = page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox'); | ||
| 34 | + this.getCodeButton = page.locator('uni-button').filter({ hasText: '获取验证码' }); | ||
| 35 | + this.loginButton = page.locator('uni-button').filter({ hasText: '登录' }); | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 导航到登录页面 | ||
| 40 | + */ | ||
| 41 | + async goto(): Promise<void> { | ||
| 42 | + await this.navigate('/pages/login/index'); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 点击手机号登录标签 | ||
| 47 | + */ | ||
| 48 | + async clickPhoneLoginTab(): Promise<void> { | ||
| 49 | + await this.phoneLoginTab.click(); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 点击确认按钮 | ||
| 54 | + */ | ||
| 55 | + async clickConfirm(): Promise<void> { | ||
| 56 | + await this.confirmButton.click(); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + /** | ||
| 60 | + * 输入手机号 | ||
| 61 | + * @param phone 手机号 | ||
| 62 | + */ | ||
| 63 | + async enterPhone(phone: string): Promise<void> { | ||
| 64 | + await this.phoneInput.click(); | ||
| 65 | + await this.phoneInput.fill(phone); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + /** | ||
| 69 | + * 点击获取验证码按钮 | ||
| 70 | + */ | ||
| 71 | + async clickGetCode(): Promise<void> { | ||
| 72 | + await this.getCodeButton.click(); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + /** | ||
| 76 | + * 点击登录按钮 | ||
| 77 | + */ | ||
| 78 | + async clickLogin(): Promise<void> { | ||
| 79 | + await this.loginButton.click(); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + /** | ||
| 83 | + * 半自动登录(自动填手机号,手动输验证码) | ||
| 84 | + * @param phone 手机号 | ||
| 85 | + * @param timeout 等待登录成功的超时时间(毫秒) | ||
| 86 | + */ | ||
| 87 | + async loginWithPhone(phone: string, timeout: number = 120000): Promise<void> { | ||
| 88 | + await this.goto(); | ||
| 89 | + await this.clickPhoneLoginTab(); | ||
| 90 | + await this.clickConfirm(); | ||
| 91 | + await this.enterPhone(phone); | ||
| 92 | + await this.clickGetCode(); | ||
| 93 | + | ||
| 94 | + console.log('请手动输入验证码,然后点击登录按钮...'); | ||
| 95 | + | ||
| 96 | + // 等待登录成功(检测用户名出现) | ||
| 97 | + await this.page.locator('uni-button').filter({ hasText: '登录' }).click(); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /** | ||
| 101 | + * 等待登录成功 | ||
| 102 | + * @param userName 期望显示的用户名 | ||
| 103 | + * @param timeout 超时时间 | ||
| 104 | + */ | ||
| 105 | + async waitForLoginSuccess(userName: string, timeout: number = 120000): Promise<void> { | ||
| 106 | + await expect(this.page.getByText(userName)).toBeVisible({ timeout }); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + /** | ||
| 110 | + * 检查是否已登录(通过检测用户名是否存在) | ||
| 111 | + * @param userName 用户名 | ||
| 112 | + */ | ||
| 113 | + async isLoggedIn(userName: string): Promise<boolean> { | ||
| 114 | + try { | ||
| 115 | + await expect(this.page.getByText(userName)).toBeVisible({ timeout: 5000 }); | ||
| 116 | + return true; | ||
| 117 | + } catch { | ||
| 118 | + return false; | ||
| 119 | + } | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * 保存认证状态到文件 | ||
| 124 | + * @param filePath 保存路径 | ||
| 125 | + */ | ||
| 126 | + async saveAuthState(filePath: string = 'auth.json'): Promise<void> { | ||
| 127 | + await this.page.context().storageState({ path: filePath }); | ||
| 128 | + console.log(`认证状态已保存到 ${filePath}`); | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + /** | ||
| 132 | + * 完整的登录流程(半自动) | ||
| 133 | + * @param phone 手机号 | ||
| 134 | + * @param userName 期望的用户名(用于验证登录成功) | ||
| 135 | + * @param authFilePath 认证文件保存路径 | ||
| 136 | + */ | ||
| 137 | + async performLogin( | ||
| 138 | + phone: string, | ||
| 139 | + userName: string = '赵xt', | ||
| 140 | + authFilePath: string = 'auth.json' | ||
| 141 | + ): Promise<void> { | ||
| 142 | + await this.loginWithPhone(phone); | ||
| 143 | + await this.waitForLoginSuccess(userName); | ||
| 144 | + await this.saveAuthState(authFilePath); | ||
| 145 | + } | ||
| 146 | +} | ||
| 0 | \ No newline at end of file | 147 | \ No newline at end of file |
pages/productPage.ts
0 → 100644
| 1 | +++ a/pages/productPage.ts | ||
| 1 | +import { Page, Locator, expect } from '@playwright/test'; | ||
| 2 | +import { BasePage } from './basePage'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 商品管理页面选择器 | ||
| 6 | + */ | ||
| 7 | +const selectors = { | ||
| 8 | + // 导航 | ||
| 9 | + productManagementMenu: 'text=商品管理', | ||
| 10 | + addProductButton: 'text=新增商品', | ||
| 11 | + | ||
| 12 | + // 表单 | ||
| 13 | + productNameInput: '.nut-input__input > .uni-input-wrapper > .uni-input-input', | ||
| 14 | + categorySearchInput: '.uni-scroll-view-content > .z-paging-content > .nut-searchbar > .nut-searchbar__search-input > .nut-searchbar__input-inner > .nut-searchbar__input-form > span > .nut-searchbar__input-bar > .uni-input-wrapper > .uni-input-input', | ||
| 15 | + categoryOption: (categoryName: string) => `uni-view:has-text("${categoryName}")`, | ||
| 16 | + saveButton: 'text=保存', | ||
| 17 | + | ||
| 18 | + // 列表 | ||
| 19 | + productInList: (productName: string) => `text=${productName}`, | ||
| 20 | +}; | ||
| 21 | + | ||
| 22 | +/** | ||
| 23 | + * 商品管理页面类 | ||
| 24 | + * 处理商品相关操作 | ||
| 25 | + */ | ||
| 26 | +export class ProductPage extends BasePage { | ||
| 27 | + // 导航定位器 | ||
| 28 | + readonly productManagementMenu: Locator; | ||
| 29 | + readonly addProductButton: Locator; | ||
| 30 | + | ||
| 31 | + // 表单定位器 | ||
| 32 | + readonly productNameInput: Locator; | ||
| 33 | + readonly categorySearchInput: Locator; | ||
| 34 | + readonly saveButton: Locator; | ||
| 35 | + | ||
| 36 | + constructor(page: Page) { | ||
| 37 | + super(page); | ||
| 38 | + | ||
| 39 | + this.productManagementMenu = page.getByText('商品管理'); | ||
| 40 | + this.addProductButton = page.getByText('新增商品'); | ||
| 41 | + this.productNameInput = page.locator('.nut-input__input > .uni-input-wrapper > .uni-input-input').first(); | ||
| 42 | + this.categorySearchInput = page.locator('.uni-scroll-view-content > .z-paging-content > .nut-searchbar > .nut-searchbar__search-input > .nut-searchbar__input-inner > .nut-searchbar__input-form > span > .nut-searchbar__input-bar > .uni-input-wrapper > .uni-input-input'); | ||
| 43 | + this.saveButton = page.getByText('保存'); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 导航到首页 | ||
| 48 | + */ | ||
| 49 | + async gotoHome(): Promise<void> { | ||
| 50 | + await this.navigate('/'); | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 打开商品管理菜单 | ||
| 55 | + */ | ||
| 56 | + async openProductManagement(): Promise<void> { | ||
| 57 | + await this.productManagementMenu.click(); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + /** | ||
| 61 | + * 点击新增商品按钮 | ||
| 62 | + */ | ||
| 63 | + async clickAddProduct(): Promise<void> { | ||
| 64 | + await this.addProductButton.click(); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + /** | ||
| 68 | + * 打开新增商品表单 | ||
| 69 | + * 完整流程:导航首页 -> 商品管理 -> 新增商品 | ||
| 70 | + */ | ||
| 71 | + async openAddProductForm(): Promise<void> { | ||
| 72 | + await this.gotoHome(); | ||
| 73 | + await this.openProductManagement(); | ||
| 74 | + await this.clickAddProduct(); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /** | ||
| 78 | + * 输入商品名称 | ||
| 79 | + * @param productName 商品名称 | ||
| 80 | + */ | ||
| 81 | + async enterProductName(productName: string): Promise<void> { | ||
| 82 | + await this.productNameInput.click(); | ||
| 83 | + await this.productNameInput.fill(productName); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 选择商品分类 | ||
| 88 | + * @param categoryName 分类名称 | ||
| 89 | + */ | ||
| 90 | + async selectCategory(categoryName: string): Promise<void> { | ||
| 91 | + // 点击分类选择器 | ||
| 92 | + await this.page.locator('.nut-input__mask').first().click(); | ||
| 93 | + | ||
| 94 | + // 搜索分类 | ||
| 95 | + await this.categorySearchInput.click(); | ||
| 96 | + await this.categorySearchInput.fill(categoryName); | ||
| 97 | + | ||
| 98 | + // 选择匹配的分类,精准匹配 | ||
| 99 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${categoryName}$`) }).first().click(); | ||
| 100 | + // 匹配包含分类名称的元素 | ||
| 101 | + // 匹配以分类名称开头的元素(允许后面有其他字符) | ||
| 102 | + // await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${categoryName}`) }).first().click(); | ||
| 103 | + | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | +// /** | ||
| 107 | +// * 选择分类 | ||
| 108 | +// * @param categoryName 分类名称 | ||
| 109 | +// */ | ||
| 110 | +// async selectCategory(categoryName: string): Promise<void> { | ||
| 111 | + | ||
| 112 | +// // 点击分类选择器 | ||
| 113 | +// await this.page.locator('.nut-input__mask').first().click(); | ||
| 114 | +// // 搜索分类 | ||
| 115 | +// await this.categorySearchInput.click(); | ||
| 116 | +// // await this.categorySearchInput.fill(categoryName); | ||
| 117 | +// // 取前两个字匹配,避免后缀影响 | ||
| 118 | +// const categoryPrefix = categoryName.substring(0, 2); | ||
| 119 | +// await this.categorySearchInput.fill(categoryPrefix); | ||
| 120 | +// // 直接用文本匹配,排除容器 | ||
| 121 | +// await this.page.getByText(new RegExp(`^${categoryPrefix}`), { exact: false }) | ||
| 122 | +// .nth(1) // 可能第一个是搜索框里的文字,用 nth(1) 选第二个 | ||
| 123 | +// .click({ force: true }); | ||
| 124 | + | ||
| 125 | + | ||
| 126 | + | ||
| 127 | + | ||
| 128 | +// } | ||
| 129 | + | ||
| 130 | + | ||
| 131 | + /** | ||
| 132 | + * 点击保存按钮 | ||
| 133 | + */ | ||
| 134 | + async clickSave(): Promise<void> { | ||
| 135 | + await this.saveButton.click(); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + /** | ||
| 139 | + * 创建新商品(完整流程) | ||
| 140 | + * @param productName 商品名称 | ||
| 141 | + * @param categoryName 分类名称 | ||
| 142 | + */ | ||
| 143 | + async createProduct(productName: string, categoryName: string): Promise<void> { | ||
| 144 | + await this.openAddProductForm(); | ||
| 145 | + await this.enterProductName(productName); | ||
| 146 | + await this.selectCategory(categoryName); | ||
| 147 | + await this.clickSave(); | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + /** | ||
| 151 | + * 验证商品是否可见 | ||
| 152 | + * @param productName 商品名称 | ||
| 153 | + */ | ||
| 154 | + async expectProductVisible(productName: string): Promise<void> { | ||
| 155 | + await expect(this.page.getByText(productName)).toBeVisible(); | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + /** | ||
| 159 | + * 验证商品创建成功 | ||
| 160 | + * @param productName 商品名称 | ||
| 161 | + */ | ||
| 162 | + async verifyProductCreated(productName: string): Promise<void> { | ||
| 163 | + await this.expectProductVisible(productName); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + /** | ||
| 167 | + * 在列表中查找商品 | ||
| 168 | + * @param productName 商品名称 | ||
| 169 | + */ | ||
| 170 | + async findProductInList(productName: string): Promise<Locator> { | ||
| 171 | + return this.page.getByText(productName); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + /** | ||
| 175 | + * 检查商品是否存在于列表 | ||
| 176 | + * @param productName 商品名称 | ||
| 177 | + */ | ||
| 178 | + async isProductInList(productName: string): Promise<boolean> { | ||
| 179 | + try { | ||
| 180 | + const product = await this.findProductInList(productName); | ||
| 181 | + await product.waitFor({ state: 'visible', timeout: 5000 }); | ||
| 182 | + return true; | ||
| 183 | + } catch { | ||
| 184 | + return false; | ||
| 185 | + } | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + /** | ||
| 189 | + * 等待商品列表加载 | ||
| 190 | + */ | ||
| 191 | + async waitForProductListLoad(): Promise<void> { | ||
| 192 | + await this.page.waitForLoadState('networkidle'); | ||
| 193 | + } | ||
| 194 | +} | ||
| 0 | \ No newline at end of file | 195 | \ No newline at end of file |
pages/salePage.ts
0 → 100644
| 1 | +++ a/pages/salePage.ts | ||
| 1 | +import { Page, Locator, expect } from '@playwright/test'; | ||
| 2 | +import { BasePage } from './basePage'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 销售开单页面选择器 | ||
| 6 | + */ | ||
| 7 | +const selectors = { | ||
| 8 | + // 导航 | ||
| 9 | + moreMenu: 'text=更多', | ||
| 10 | + saleOrderMenu: 'text=销售开单', | ||
| 11 | + | ||
| 12 | + // 切换类型 | ||
| 13 | + consignmentTab: 'uni-view:has-text("代卖")', | ||
| 14 | + | ||
| 15 | + // 商品选择 | ||
| 16 | + productOption: (productName: string) => `text=${productName}`, | ||
| 17 | + selectBatchButton: 'text=请选择批次', | ||
| 18 | + batchList: '.batch-list .batch-item', | ||
| 19 | + confirmButton: 'text=确认', | ||
| 20 | + | ||
| 21 | + // 数量和价格输入 | ||
| 22 | + quantityInput: 'uni-view:has-text("斤")', | ||
| 23 | + priceInput: 'uni-view:has-text("元/斤")', | ||
| 24 | + numberKey: (key: string) => `.number-keyboard uni-view[data-key="${key}"]`, | ||
| 25 | + completeButton: 'text=完成', | ||
| 26 | + | ||
| 27 | + // 客户选择 | ||
| 28 | + selectCustomerButton: 'text=选择客户', | ||
| 29 | + customerItem: '.customer-item', | ||
| 30 | + | ||
| 31 | + // 支付 | ||
| 32 | + collectPaymentButton: 'text=收款', | ||
| 33 | + payWayItem: '.pay-way-item', | ||
| 34 | + confirmPaymentButton: 'text=确定', | ||
| 35 | + | ||
| 36 | + // 成功提示 | ||
| 37 | + successMessage: 'text=收款成功', | ||
| 38 | +}; | ||
| 39 | + | ||
| 40 | +/** | ||
| 41 | + * 销售开单页面类 | ||
| 42 | + * 处理销售开单相关操作 | ||
| 43 | + */ | ||
| 44 | +export class SalePage extends BasePage { | ||
| 45 | + // 导航定位器 | ||
| 46 | + readonly moreMenu: Locator; | ||
| 47 | + readonly saleOrderMenu: Locator; | ||
| 48 | + | ||
| 49 | + // 切换类型 | ||
| 50 | + readonly consignmentTab: Locator; | ||
| 51 | + | ||
| 52 | + // 批次选择 | ||
| 53 | + readonly selectBatchButton: Locator; | ||
| 54 | + readonly batchList: Locator; | ||
| 55 | + readonly confirmButton: Locator; | ||
| 56 | + | ||
| 57 | + // 客户选择 | ||
| 58 | + readonly selectCustomerButton: Locator; | ||
| 59 | + readonly customerItem: Locator; | ||
| 60 | + | ||
| 61 | + // 支付 | ||
| 62 | + readonly collectPaymentButton: Locator; | ||
| 63 | + readonly payWayItem: Locator; | ||
| 64 | + readonly confirmPaymentButton: Locator; | ||
| 65 | + readonly completeButton: Locator; | ||
| 66 | + | ||
| 67 | + constructor(page: Page) { | ||
| 68 | + super(page); | ||
| 69 | + | ||
| 70 | + this.moreMenu = page.getByText('更多'); | ||
| 71 | + this.saleOrderMenu = page.getByText('销售开单'); | ||
| 72 | + this.consignmentTab = page.locator('uni-view').filter({ hasText: /^代卖$/ }).first(); | ||
| 73 | + this.selectBatchButton = page.getByText('请选择批次'); | ||
| 74 | + this.batchList = page.locator('.batch-list .batch-item'); | ||
| 75 | + this.confirmButton = page.getByText('确认'); | ||
| 76 | + this.selectCustomerButton = page.getByText('选择客户'); | ||
| 77 | + this.customerItem = page.locator('.customer-item'); | ||
| 78 | + this.collectPaymentButton = page.getByText('收款'); | ||
| 79 | + this.payWayItem = page.locator('.pay-way-item'); | ||
| 80 | + this.confirmPaymentButton = page.getByText('确定'); | ||
| 81 | + this.completeButton = page.getByText('完成'); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 导航到首页并等待加载 | ||
| 86 | + */ | ||
| 87 | + async gotoHome(): Promise<void> { | ||
| 88 | + await this.navigate('/'); | ||
| 89 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * 打开更多菜单 | ||
| 94 | + */ | ||
| 95 | + async openMoreMenu(): Promise<void> { | ||
| 96 | + await this.moreMenu.click(); | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + /** | ||
| 100 | + * 打开销售开单页面 | ||
| 101 | + */ | ||
| 102 | + async openSaleOrder(): Promise<void> { | ||
| 103 | + await this.saleOrderMenu.click(); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + /** | ||
| 107 | + * 导航到销售开单(完整流程) | ||
| 108 | + */ | ||
| 109 | + async navigateToSaleOrder(): Promise<void> { | ||
| 110 | + await this.gotoHome(); | ||
| 111 | + await this.openMoreMenu(); | ||
| 112 | + await this.openSaleOrder(); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + /** | ||
| 116 | + * 切换到代卖模式 | ||
| 117 | + */ | ||
| 118 | + async switchToConsignment(): Promise<void> { | ||
| 119 | + await this.consignmentTab.click(); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * 选择商品 | ||
| 124 | + * @param productName 商品名称 | ||
| 125 | + */ | ||
| 126 | + async selectProduct(productName: string): Promise<void> { | ||
| 127 | + await this.page.getByText(productName).click(); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + /** | ||
| 131 | + * 打开批次选择弹窗 | ||
| 132 | + */ | ||
| 133 | + async openBatchSelection(): Promise<void> { | ||
| 134 | + await this.selectBatchButton.click(); | ||
| 135 | + await this.batchList.first().waitFor({ state: 'attached', timeout: 10000 }); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + /** | ||
| 139 | + * 选择批次 | ||
| 140 | + * @param batchIndex 批次索引(可选,不传则随机选择) | ||
| 141 | + */ | ||
| 142 | + async selectBatch(batchIndex?: number): Promise<number> { | ||
| 143 | + // 确保批次列表已打开 | ||
| 144 | + const count = await this.batchList.count(); | ||
| 145 | + if (count === 0) { | ||
| 146 | + throw new Error('未找到任何批次项'); | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + const selectedIndex = batchIndex ?? Math.floor(Math.random() * count); | ||
| 150 | + console.log(`选择第 ${selectedIndex + 1} 个批次项`); | ||
| 151 | + | ||
| 152 | + await this.batchList.nth(selectedIndex).click(); | ||
| 153 | + return selectedIndex; | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + /** | ||
| 157 | + * 确认批次选择 | ||
| 158 | + */ | ||
| 159 | + async confirmBatchSelection(): Promise<void> { | ||
| 160 | + await this.confirmButton.click(); | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + /** | ||
| 164 | + * 输入数量 | ||
| 165 | + * @param quantity 数量(数字字符串) | ||
| 166 | + */ | ||
| 167 | + async enterQuantity(quantity: string): Promise<void> { | ||
| 168 | + // 点击数量输入框 | ||
| 169 | + await this.page.locator('uni-view').filter({ hasText: /^斤$/ }).first().click(); | ||
| 170 | + | ||
| 171 | + // 输入数字 | ||
| 172 | + for (const digit of quantity) { | ||
| 173 | + await this.page.locator(`.number-keyboard uni-view[data-key="${digit}"]`).click(); | ||
| 174 | + } | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + /** | ||
| 178 | + * 输入单价 | ||
| 179 | + * @param price 单价(数字字符串) | ||
| 180 | + */ | ||
| 181 | + async enterPrice(price: string): Promise<void> { | ||
| 182 | + // 点击单价输入框 | ||
| 183 | + await this.page.locator('uni-view').filter({ hasText: /^元\/斤$/ }).first().click(); | ||
| 184 | + | ||
| 185 | + // 输入数字 | ||
| 186 | + for (const digit of price) { | ||
| 187 | + await this.page.locator(`.number-keyboard uni-view[data-key="${digit}"]`).click(); | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + /** | ||
| 192 | + * 点击完成按钮(商品选择完成) | ||
| 193 | + */ | ||
| 194 | + async clickComplete(): Promise<void> { | ||
| 195 | + await this.completeButton.click(); | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + /** | ||
| 199 | + * 打开客户选择 | ||
| 200 | + */ | ||
| 201 | + async openCustomerSelection(): Promise<void> { | ||
| 202 | + await this.selectCustomerButton.click(); | ||
| 203 | + await this.customerItem.first().waitFor({ state: 'attached' }); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + /** | ||
| 207 | + * 选择客户 | ||
| 208 | + * @param customerIndex 客户索引(默认选择第一个) | ||
| 209 | + */ | ||
| 210 | + async selectCustomer(customerIndex: number = 0): Promise<void> { | ||
| 211 | + await this.customerItem.nth(customerIndex).click(); | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + /** | ||
| 215 | + * 点击收款按钮 | ||
| 216 | + */ | ||
| 217 | + async clickCollectPayment(): Promise<void> { | ||
| 218 | + await this.collectPaymentButton.click(); | ||
| 219 | + await this.payWayItem.first().waitFor({ state: 'attached' }); | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + /** | ||
| 223 | + * 选择支付方式 | ||
| 224 | + * @param payWayIndex 支付方式索引(默认选择第一个) | ||
| 225 | + */ | ||
| 226 | + async selectPayWay(payWayIndex: number = 0): Promise<void> { | ||
| 227 | + await this.payWayItem.nth(payWayIndex).click(); | ||
| 228 | + } | ||
| 229 | + | ||
| 230 | + /** | ||
| 231 | + * 确认支付 | ||
| 232 | + */ | ||
| 233 | + async confirmPayment(): Promise<void> { | ||
| 234 | + await this.confirmPaymentButton.click(); | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + /** | ||
| 238 | + * 验证收款成功 | ||
| 239 | + */ | ||
| 240 | + async expectPaymentSuccess(): Promise<void> { | ||
| 241 | + await expect(this.page.getByText('收款成功')).toBeVisible(); | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + /** | ||
| 245 | + * 完整的销售开单流程(代卖模式) | ||
| 246 | + * @param productName 商品名称 | ||
| 247 | + * @param quantity 数量 | ||
| 248 | + * @param price 单价 | ||
| 249 | + * @param customerIndex 客户索引 | ||
| 250 | + * @param payWayIndex 支付方式索引 | ||
| 251 | + */ | ||
| 252 | + async createConsignmentSaleOrder( | ||
| 253 | + productName: string, | ||
| 254 | + quantity: string = '1', | ||
| 255 | + price: string = '1', | ||
| 256 | + customerIndex: number = 0, | ||
| 257 | + payWayIndex: number = 0 | ||
| 258 | + ): Promise<void> { | ||
| 259 | + // 导航 | ||
| 260 | + await this.navigateToSaleOrder(); | ||
| 261 | + | ||
| 262 | + // 切换代卖 | ||
| 263 | + await this.switchToConsignment(); | ||
| 264 | + | ||
| 265 | + // 选择商品 | ||
| 266 | + await this.selectProduct(productName); | ||
| 267 | + | ||
| 268 | + // 选择批次 | ||
| 269 | + await this.openBatchSelection(); | ||
| 270 | + await this.selectBatch(); | ||
| 271 | + await this.confirmBatchSelection(); | ||
| 272 | + | ||
| 273 | + // 输入数量和价格 | ||
| 274 | + await this.enterQuantity(quantity); | ||
| 275 | + await this.enterPrice(price); | ||
| 276 | + await this.clickComplete(); | ||
| 277 | + | ||
| 278 | + // 选择客户 | ||
| 279 | + await this.openCustomerSelection(); | ||
| 280 | + await this.selectCustomer(customerIndex); | ||
| 281 | + | ||
| 282 | + // 收款 | ||
| 283 | + await this.clickCollectPayment(); | ||
| 284 | + await this.selectPayWay(payWayIndex); | ||
| 285 | + await this.confirmPayment(); | ||
| 286 | + | ||
| 287 | + // 验证成功 | ||
| 288 | + await this.expectPaymentSuccess(); | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + /** | ||
| 292 | + * 获取批次数量 | ||
| 293 | + */ | ||
| 294 | + async getBatchCount(): Promise<number> { | ||
| 295 | + await this.batchList.first().waitFor({ state: 'attached', timeout: 10000 }); | ||
| 296 | + return this.batchList.count(); | ||
| 297 | + } | ||
| 298 | + | ||
| 299 | + /** | ||
| 300 | + * 获取客户数量 | ||
| 301 | + */ | ||
| 302 | + async getCustomerCount(): Promise<number> { | ||
| 303 | + await this.customerItem.first().waitFor({ state: 'attached' }); | ||
| 304 | + return this.customerItem.count(); | ||
| 305 | + } | ||
| 306 | +} | ||
| 0 | \ No newline at end of file | 307 | \ No newline at end of file |
playwright.config.ts
0 → 100644
| 1 | +++ a/playwright.config.ts | ||
| 1 | +import { defineConfig, devices } from '@playwright/test'; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * Read environment variables from file. | ||
| 5 | + * https://github.com/motdotla/dotenv | ||
| 6 | + */ | ||
| 7 | +import dotenv from 'dotenv'; | ||
| 8 | +import path from 'path'; | ||
| 9 | +dotenv.config({path: | ||
| 10 | + path.resolve(__dirname,'.env')}); | ||
| 11 | +// dotenv.config({ path: path.resolve(__dirname, '.env') }); | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * See https://playwright.dev/docs/test-configuration. | ||
| 15 | + */ | ||
| 16 | +export default defineConfig({ | ||
| 17 | + testDir: './tests', | ||
| 18 | + /* Run tests in files in parallel */ | ||
| 19 | + fullyParallel: true, | ||
| 20 | + /* Fail the build on CI if you accidentally left test.only in the source code. */ | ||
| 21 | + forbidOnly: !!process.env.CI, | ||
| 22 | + /* Retry on CI only */ | ||
| 23 | + retries: process.env.CI ? 2 : 0, | ||
| 24 | + /* Opt out of parallel tests on CI. */ | ||
| 25 | + workers: process.env.CI ? 1 : undefined, | ||
| 26 | + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ | ||
| 27 | + reporter: [ | ||
| 28 | + ['line'], | ||
| 29 | + ['html'], | ||
| 30 | + ['allure-playwright',{ | ||
| 31 | + detail:true, | ||
| 32 | + outputFolder:'allure-results', | ||
| 33 | + environmentInfo:{ | ||
| 34 | + os:process.platform, | ||
| 35 | + node_version:process.version, | ||
| 36 | + } | ||
| 37 | + }] | ||
| 38 | + | ||
| 39 | + ], | ||
| 40 | + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | ||
| 41 | + use: { | ||
| 42 | + /* Base URL to use in actions like `await page.goto('')`. */ | ||
| 43 | + baseURL: process.env.BASE_URL, | ||
| 44 | + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ | ||
| 45 | + trace: 'on-first-retry', | ||
| 46 | + }, | ||
| 47 | + | ||
| 48 | + /* Configure projects for major browsers */ | ||
| 49 | + projects: [ | ||
| 50 | + { | ||
| 51 | + name: 'chromium', | ||
| 52 | + use: { ...devices['Desktop Chrome'] }, | ||
| 53 | + }, | ||
| 54 | + | ||
| 55 | + // { | ||
| 56 | + // name: 'firefox', | ||
| 57 | + // use: { ...devices['Desktop Firefox'] }, | ||
| 58 | + // }, | ||
| 59 | + | ||
| 60 | + // { | ||
| 61 | + // name: 'webkit', | ||
| 62 | + // use: { ...devices['Desktop Safari'] }, | ||
| 63 | + // }, | ||
| 64 | + | ||
| 65 | + /* Test against mobile viewports. */ | ||
| 66 | + // { | ||
| 67 | + // name: 'Mobile Chrome', | ||
| 68 | + // use: { ...devices['Pixel 5'] }, | ||
| 69 | + // }, | ||
| 70 | + // { | ||
| 71 | + // name: 'Mobile Safari', | ||
| 72 | + // use: { ...devices['iPhone 12'] }, | ||
| 73 | + // }, | ||
| 74 | + | ||
| 75 | + /* Test against branded browsers. */ | ||
| 76 | + // { | ||
| 77 | + // name: 'Microsoft Edge', | ||
| 78 | + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, | ||
| 79 | + // }, | ||
| 80 | + // { | ||
| 81 | + // name: 'Google Chrome', | ||
| 82 | + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, | ||
| 83 | + // }, | ||
| 84 | + ], | ||
| 85 | + | ||
| 86 | + /* Run your local dev server before starting the tests */ | ||
| 87 | + // webServer: { | ||
| 88 | + // command: 'npm run start', | ||
| 89 | + // url: 'http://localhost:3000', | ||
| 90 | + // reuseExistingServer: !process.env.CI, | ||
| 91 | + // }, | ||
| 92 | +}); |
scripts/run-tests.ts
0 → 100644
| 1 | +++ a/scripts/run-tests.ts |
scripts/save-auth.ts
0 → 100644
| 1 | +++ a/scripts/save-auth.ts | ||
| 1 | +// 半自动登录,登录存放auth.json | ||
| 2 | +import { test, expect } from '@playwright/test'; | ||
| 3 | + | ||
| 4 | +test('半自动登录(自动填手机号,手动输验证码)', async ({ page }) => { | ||
| 5 | + await page.goto('/#/pages/login/index'); | ||
| 6 | + await page.getByText('手机号登录/注册').click(); | ||
| 7 | + await page.getByText('确定').click(); | ||
| 8 | + // --- 这里执行登录操作 --- | ||
| 9 | + await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').click(); | ||
| 10 | + await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').fill('13548301969'); | ||
| 11 | + // 此处假设出现了验证码,你需要手动输入或处理 | ||
| 12 | + await page.locator('uni-button').filter({ hasText: '获取验证码' }).click(); | ||
| 13 | + console.log('请手动输入验证码,然后点击登录按钮...'); | ||
| 14 | + await page.locator('uni-button').filter({ hasText: '登录' }).click(); | ||
| 15 | + // --- 等待登录成功,跳转到首页 --- | ||
| 16 | + await expect(page.getByText('赵xt')).toBeVisible({ timeout: 0 }); | ||
| 17 | + | ||
| 18 | + // 4. 保存存储状态(Cookie 和 LocalStorage) | ||
| 19 | + await page.context().storageState({ path: 'auth.json' }); | ||
| 20 | + console.log('状态已保存到 auth.json'); | ||
| 21 | +}); | ||
| 0 | \ No newline at end of file | 22 | \ No newline at end of file |
test-data/user.json
0 → 100644
| 1 | +++ a/test-data/user.json |
tests/consignmentOrder.spec.ts
0 → 100644
| 1 | +++ a/tests/consignmentOrder.spec.ts | ||
| 1 | +import { test, expect } from '../fixtures'; | ||
| 2 | +import { generateOtherName } from '../utils/dataGenerator'; | ||
| 3 | +import * as allure from 'allure-js-commons'; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 代销入库测试 | ||
| 7 | + */ | ||
| 8 | +test.describe('代销入库', () => { | ||
| 9 | + // 使用已保存的认证状态 | ||
| 10 | + test.use({ storageState: 'auth.json' }); | ||
| 11 | + | ||
| 12 | + test('新增代销入库 - 完整流程', async ({ consignmentPage },testInfo) => { | ||
| 13 | + // 添加allure元素 | ||
| 14 | + await allure.epic('代销管理'); | ||
| 15 | + await allure.feature('代销入库'); | ||
| 16 | + await allure.story('创建代销入库订单'); | ||
| 17 | + // 步骤1,生成批次别名 | ||
| 18 | + const batchAlias = await allure.step('生成唯一批次别名',async(step)=>{ | ||
| 19 | + const alias = generateOtherName('自代卖'); | ||
| 20 | + console.log('生成的批次别名:', alias); | ||
| 21 | + return alias; | ||
| 22 | + }); | ||
| 23 | + // 生成唯一批次别名 | ||
| 24 | + // const batchAlias = generateOtherName('代卖'); | ||
| 25 | + // console.log('生成的批次别名:', batchAlias); | ||
| 26 | + | ||
| 27 | + // 步骤2.执行代销入库 | ||
| 28 | + await allure.step('填写并提交代销入库表单',async()=>{ | ||
| 29 | + await consignmentPage.createConsignmentOrder( | ||
| 30 | + batchAlias, // 批次别名 | ||
| 31 | + '娃娃菜', // 商品名称 | ||
| 32 | + '10', // 数量 | ||
| 33 | + '1' // 费用金额 | ||
| 34 | + ); | ||
| 35 | + await consignmentPage.attachScreenshot(testInfo,'代销入库成功截图'); | ||
| 36 | + }) | ||
| 37 | + // 使用页面对象创建代销入库 | ||
| 38 | + // await consignmentPage.createConsignmentOrder( | ||
| 39 | + // batchAlias, // 批次别名 | ||
| 40 | + // '娃娃菜', // 商品名称 | ||
| 41 | + // '10', // 数量 | ||
| 42 | + // '1' // 费用金额 | ||
| 43 | + // ); | ||
| 44 | + }); | ||
| 45 | + | ||
| 46 | + // test('新增代销入库 - 简化流程(无费用)', async ({ consignmentPage }) => { | ||
| 47 | + // // 生成唯一批次别名 | ||
| 48 | + // const batchAlias = generateOtherName('代卖'); | ||
| 49 | + // console.log('生成的批次别名:', batchAlias); | ||
| 50 | + | ||
| 51 | + // // 使用简化流程创建代销入库 | ||
| 52 | + // await consignmentPage.createSimpleConsignmentOrder( | ||
| 53 | + // batchAlias, // 批次别名 | ||
| 54 | + // '娃娃菜' // 商品名称 | ||
| 55 | + // ); | ||
| 56 | + | ||
| 57 | + // // 验证批次创建成功 | ||
| 58 | + // await consignmentPage.expectBatchCreated(batchAlias); | ||
| 59 | + // }); | ||
| 60 | + | ||
| 61 | + // test('新增代销入库 - 分步操作示例', async ({ consignmentPage }) => { | ||
| 62 | + // // 生成唯一批次别名 | ||
| 63 | + // const batchAlias = generateOtherName('代卖'); | ||
| 64 | + // console.log('生成的批次别名:', batchAlias); | ||
| 65 | + | ||
| 66 | + // // 导航到新增页面 | ||
| 67 | + // await consignmentPage.navigateToNewConsignment(); | ||
| 68 | + | ||
| 69 | + // // 选择仓库 | ||
| 70 | + // await consignmentPage.selectWarehouse(); | ||
| 71 | + // await consignmentPage.selectSecondWarehouse(); | ||
| 72 | + | ||
| 73 | + // // 输入批次别名 | ||
| 74 | + // await consignmentPage.enterBatchAlias(batchAlias); | ||
| 75 | + | ||
| 76 | + // // 选择商品并输入数量 | ||
| 77 | + // await consignmentPage.selectProductWithQuantity('娃娃菜', '10'); | ||
| 78 | + | ||
| 79 | + // // 添加费用 | ||
| 80 | + // await consignmentPage.addExpense(0, '1'); | ||
| 81 | + // await consignmentPage.selectPaymentMethod(0); | ||
| 82 | + | ||
| 83 | + // // 点击创建 | ||
| 84 | + // await consignmentPage.clickCreate(); | ||
| 85 | + // await consignmentPage.waitForCreationComplete(); | ||
| 86 | + | ||
| 87 | + // // 验证 | ||
| 88 | + // await consignmentPage.expectBatchCreated(batchAlias); | ||
| 89 | + // }); | ||
| 90 | +}); | ||
| 0 | \ No newline at end of file | 91 | \ No newline at end of file |
tests/login.setup.ts
0 → 100644
| 1 | +++ a/tests/login.setup.ts | ||
| 1 | +import { test as setup, expect } from '@playwright/test'; | ||
| 2 | +import path from 'path'; | ||
| 3 | + | ||
| 4 | +const authFile = path.join(__dirname, '../auth.json'); | ||
| 5 | + | ||
| 6 | +setup('authenticate', async ({ page }) => { | ||
| 7 | + // 1. 先检查 auth.json 是否存在 | ||
| 8 | + // 如果存在,并且你想让它自动跳过,可以加一个判断。这里简单起见,总是执行手动登录。 | ||
| 9 | + // 实际项目中可以加一个环境变量来控制是否强制重新登录。 | ||
| 10 | + | ||
| 11 | + console.log('开始执行认证设置...'); | ||
| 12 | + | ||
| 13 | + // 2. 访问登录页 | ||
| 14 | + await page.goto('https://erp-pad.test.gszdtop.com/#/'); | ||
| 15 | + | ||
| 16 | + // 3. 手动登录(这里可以加一些提示) | ||
| 17 | + //console.log('请手动完成手机验证码登录...'); | ||
| 18 | + | ||
| 19 | + // 4. 等待登录成功,检测某个登录后才会出现的元素 | ||
| 20 | + await expect(page.getByText('赵xt')).toBeVisible(); | ||
| 21 | +// await page.waitForSelector('text=个人中心', { timeout: 60000 }); | ||
| 22 | + | ||
| 23 | + // 5. 登录成功后,保存状态 | ||
| 24 | +// await page.context().storageState({ path: authFile }); | ||
| 25 | +// console.log('认证状态已保存到', authFile); | ||
| 26 | +}); |
tests/product.spec.ts
0 → 100644
| 1 | +++ a/tests/product.spec.ts | ||
| 1 | +import { test, expect } from '../fixtures'; | ||
| 2 | +import { ProductPage } from '../pages/productPage'; | ||
| 3 | +import { generateUniqueProductName } from '../utils/dataGenerator'; | ||
| 4 | +import * as allure from 'allure-js-commons'; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * 商品管理测试 | ||
| 8 | + */ | ||
| 9 | +test.describe('商品管理', () => { | ||
| 10 | + // 使用已保存的认证状态 | ||
| 11 | + test.use({ storageState: 'auth.json' }); | ||
| 12 | + | ||
| 13 | + test('新增商品', async ({ productPage }) => { | ||
| 14 | + // 添加allure元素 | ||
| 15 | + await allure.epic('基础设置'); | ||
| 16 | + await allure.feature('商品管理'); | ||
| 17 | + await allure.story('新建商品'); | ||
| 18 | + | ||
| 19 | + // 步骤1,生成唯一商品名 | ||
| 20 | + const productName = await allure.step('生成唯一商品名',async(step)=>{ | ||
| 21 | + const name = generateUniqueProductName('蓝莓'); | ||
| 22 | + return name | ||
| 23 | + }); | ||
| 24 | + | ||
| 25 | + // const productName = generateUniqueProductName('蓝莓'); | ||
| 26 | + | ||
| 27 | + // 步骤2,使用页面对象创建商品 | ||
| 28 | + await allure.step('新增商品',async()=>{ | ||
| 29 | + await productPage.createProduct(productName, '蓝莓'); | ||
| 30 | + }) | ||
| 31 | + // await productPage.createProduct(productName, '蓝莓'); | ||
| 32 | + | ||
| 33 | + // 步骤3,验证商品创建成功 | ||
| 34 | + await allure.step('检查新增商品',async()=>{ | ||
| 35 | + await productPage.expectProductVisible(productName); | ||
| 36 | + }) | ||
| 37 | + // await productPage.expectProductVisible(productName); | ||
| 38 | + }); | ||
| 39 | + | ||
| 40 | + // test('新增商品 - 使用分步操作', async ({ productPage }) => { | ||
| 41 | + // // 生成唯一商品名 | ||
| 42 | + // const productName = generateUniqueProductName('苹果'); | ||
| 43 | + | ||
| 44 | + // // 分步操作示例 | ||
| 45 | + // await productPage.openAddProductForm(); | ||
| 46 | + // await productPage.enterProductName(productName); | ||
| 47 | + // await productPage.selectCategory('苹果'); | ||
| 48 | + // await productPage.clickSave(); | ||
| 49 | + | ||
| 50 | + // // 验证商品创建成功 | ||
| 51 | + // await productPage.expectProductVisible(productName); | ||
| 52 | + // }); | ||
| 53 | +}); | ||
| 0 | \ No newline at end of file | 54 | \ No newline at end of file |
tests/sold.spec.ts
0 → 100644
| 1 | +++ a/tests/sold.spec.ts | ||
| 1 | +import { test, expect } from '../fixtures'; | ||
| 2 | +import * as allure from 'allure-js-commons'; | ||
| 3 | +import { BasePage } from '../pages/basePage'; | ||
| 4 | +/** | ||
| 5 | + * 销售开单测试 | ||
| 6 | + */ | ||
| 7 | +test.describe('销售开单', () => { | ||
| 8 | + // 使用已保存的认证状态 | ||
| 9 | + test.use({ storageState: 'auth.json' }); | ||
| 10 | + | ||
| 11 | + test('销售开单 - 代卖模式', async ({ salePage },testInfo) => { | ||
| 12 | + await allure.epic('销售管理'); | ||
| 13 | + await allure.feature('销售开单'); | ||
| 14 | + await allure.story('代卖模式下销售开单'); | ||
| 15 | + // 使用页面对象创建代卖销售订单 | ||
| 16 | + await allure.step('创建销售开单',async()=>{ | ||
| 17 | + await salePage.createConsignmentSaleOrder( | ||
| 18 | + '娃娃菜', // 商品名称 | ||
| 19 | + '1', // 数量 | ||
| 20 | + '1' // 单价 | ||
| 21 | + ); | ||
| 22 | + await salePage.attachScreenshot(testInfo,'收款成功截图'); | ||
| 23 | + }); | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + }); | ||
| 27 | + | ||
| 28 | + // test('销售开单 - 分步操作示例', async ({ salePage }) => { | ||
| 29 | + // // 导航到销售开单页面 | ||
| 30 | + // await salePage.navigateToSaleOrder(); | ||
| 31 | + | ||
| 32 | + // // 切换到代卖模式 | ||
| 33 | + // await salePage.switchToConsignment(); | ||
| 34 | + | ||
| 35 | + // // 选择商品 | ||
| 36 | + // await salePage.selectProduct('娃娃菜'); | ||
| 37 | + | ||
| 38 | + // // 选择批次(随机选择) | ||
| 39 | + // const batchIndex = await salePage.selectBatch(); | ||
| 40 | + // console.log(`已选择批次索引: ${batchIndex}`); | ||
| 41 | + // await salePage.confirmBatchSelection(); | ||
| 42 | + | ||
| 43 | + // // 输入数量和单价 | ||
| 44 | + // await salePage.enterQuantity('1'); | ||
| 45 | + // await salePage.enterPrice('1'); | ||
| 46 | + // await salePage.clickComplete(); | ||
| 47 | + | ||
| 48 | + // // 选择客户(选择第一个) | ||
| 49 | + // await salePage.openCustomerSelection(); | ||
| 50 | + // await salePage.selectCustomer(0); | ||
| 51 | + | ||
| 52 | + // // 收款 | ||
| 53 | + // await salePage.clickCollectPayment(); | ||
| 54 | + // await salePage.selectPayWay(0); | ||
| 55 | + // await salePage.confirmPayment(); | ||
| 56 | + | ||
| 57 | + // // 验证收款成功 | ||
| 58 | + // await salePage.expectPaymentSuccess(); | ||
| 59 | + // }); | ||
| 60 | + | ||
| 61 | + // test('销售开单 - 指定批次', async ({ salePage }) => { | ||
| 62 | + // // 导航到销售开单页面 | ||
| 63 | + // await salePage.navigateToSaleOrder(); | ||
| 64 | + | ||
| 65 | + // // 切换到代卖模式 | ||
| 66 | + // await salePage.switchToConsignment(); | ||
| 67 | + | ||
| 68 | + // // 选择商品 | ||
| 69 | + // await salePage.selectProduct('娃娃菜'); | ||
| 70 | + | ||
| 71 | + // // 获取可用批次数 | ||
| 72 | + // const batchCount = await salePage.getBatchCount(); | ||
| 73 | + // console.log(`可用批次数: ${batchCount}`); | ||
| 74 | + | ||
| 75 | + // // 选择第一个批次 | ||
| 76 | + // await salePage.selectBatch(0); | ||
| 77 | + // await salePage.confirmBatchSelection(); | ||
| 78 | + | ||
| 79 | + // // 输入数量和单价 | ||
| 80 | + // await salePage.enterQuantity('2'); | ||
| 81 | + // await salePage.enterPrice('3'); | ||
| 82 | + // await salePage.clickComplete(); | ||
| 83 | + | ||
| 84 | + // // 选择客户 | ||
| 85 | + // await salePage.openCustomerSelection(); | ||
| 86 | + // await salePage.selectCustomer(0); | ||
| 87 | + | ||
| 88 | + // // 收款 | ||
| 89 | + // await salePage.clickCollectPayment(); | ||
| 90 | + // await salePage.selectPayWay(0); | ||
| 91 | + // await salePage.confirmPayment(); | ||
| 92 | + | ||
| 93 | + // // 验证收款成功 | ||
| 94 | + // await salePage.expectPaymentSuccess(); | ||
| 95 | + // }); | ||
| 96 | +}); | ||
| 0 | \ No newline at end of file | 97 | \ No newline at end of file |
tsconfig.json
0 → 100644
| 1 | +++ a/tsconfig.json | ||
| 1 | +{ | ||
| 2 | + "compilerOptions": { | ||
| 3 | + "target": "ES2022", | ||
| 4 | + "module": "NodeNext", | ||
| 5 | + "moduleResolution": "NodeNext", | ||
| 6 | + "lib": ["ES2022"], | ||
| 7 | + "strict": true, | ||
| 8 | + "esModuleInterop": true, | ||
| 9 | + "skipLibCheck": true, | ||
| 10 | + "forceConsistentCasingInFileNames": true, | ||
| 11 | + "noEmit": true, | ||
| 12 | + "resolveJsonModule": true, | ||
| 13 | + "declaration": true, | ||
| 14 | + "declarationMap": true, | ||
| 15 | + "sourceMap": true, | ||
| 16 | + "outDir": "./dist", | ||
| 17 | + "rootDir": ".", | ||
| 18 | + "types": ["node"] | ||
| 19 | + }, | ||
| 20 | + "include": [ | ||
| 21 | + "pages/**/*.ts", | ||
| 22 | + "fixtures/**/*.ts", | ||
| 23 | + "utils/**/*.ts", | ||
| 24 | + "tests/**/*.ts", | ||
| 25 | + "ai/**/*.ts", | ||
| 26 | + "scripts/**/*.ts" | ||
| 27 | + ], | ||
| 28 | + "exclude": [ | ||
| 29 | + "node_modules", | ||
| 30 | + "dist" | ||
| 31 | + ] | ||
| 32 | +} | ||
| 0 | \ No newline at end of file | 33 | \ No newline at end of file |
utils/aiClient.ts
0 → 100644
| 1 | +++ a/utils/aiClient.ts |
utils/dataGenerator.ts
0 → 100644
| 1 | +++ a/utils/dataGenerator.ts | ||
| 1 | +//商品管理-随机商品名 | ||
| 2 | +export function generateUniqueProductName(baseName: string = '测试商品'): string { | ||
| 3 | + // 使用时间戳,精确到毫秒,保证每次都不一样 | ||
| 4 | +// const timestamp = Date.now(); | ||
| 5 | + // 再加一个随机数,防止同一毫秒内多次调用 | ||
| 6 | + const random = Math.floor(Math.random() * 100); | ||
| 7 | + return `${baseName}_${random}`; | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +//代销入库-随机批次号 | ||
| 11 | +export function generateOtherName(baseName: string = '批次别名'): string { | ||
| 12 | + // 使用时间戳,精确到毫秒,保证每次都不一样 | ||
| 13 | + const timestamp = Date.now(); | ||
| 14 | + // 再加一个随机数,防止同一毫秒内多次调用 | ||
| 15 | +// const random = Math.floor(Math.random() * 100); | ||
| 16 | + return `${baseName}${timestamp}`; | ||
| 17 | +} | ||
| 0 | \ No newline at end of file | 18 | \ No newline at end of file |
utils/logger.ts
0 → 100644
| 1 | +++ a/utils/logger.ts |
utils/screenshot..ts
0 → 100644
| 1 | +++ a/utils/screenshot..ts |