Commit 33d656b8873320c7038b11a7908b0b1271253f02

Authored by 赵旭婷
0 parents

xfbh

.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
  1 +++ a/.gitignore
  1 +
  2 +# Playwright
  3 +node_modules/
  4 +/test-results/
  5 +/playwright-report/
  6 +/blob-report/
  7 +/playwright/.cache/
  8 +/playwright/.auth/
  9 +/auth.json
  10 +/.env
  11 +/allure-results/
  12 +/plans/
  13 +/reports/
0 14 \ No newline at end of file
... ...
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
... ...