Commit 75680a318acf8a2fac1855dedb6aca403f6d4ab7
Merge branch 'xfbhlyx' into 'master'
Xfbhlyx 合并 See merge request !1
Showing
8 changed files
with
1289 additions
and
0 deletions
CLAUDE.md
0 → 100644
| 1 | +# CLAUDE.md | ||
| 2 | + | ||
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
| 4 | + | ||
| 5 | +## Project Overview | ||
| 6 | + | ||
| 7 | +Playwright-based UI test automation framework for a business management system (supplier, customer, product, sales management). Uses page object pattern with Allure reporting. | ||
| 8 | + | ||
| 9 | +## Commands | ||
| 10 | + | ||
| 11 | +```bash | ||
| 12 | +npm install # Install dependencies | ||
| 13 | +npm run test:ci # Run tests in CI mode | ||
| 14 | +npm run report # View Allure test reports | ||
| 15 | +npm run save-auth # Save authentication state to auth.json | ||
| 16 | +``` | ||
| 17 | + | ||
| 18 | +Run a single test file: | ||
| 19 | +```bash | ||
| 20 | +npx playwright test tests/customer.spec.ts | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +## Architecture | ||
| 24 | + | ||
| 25 | +### Fixture System | ||
| 26 | + | ||
| 27 | +Two parallel fixture systems exist in `fixtures/testFixture.ts`: | ||
| 28 | + | ||
| 29 | +1. **`test`** - Standard Playwright test with regular page | ||
| 30 | +2. **`fullTest`** - Uses `authenticatedPage` from `authFixture.ts` (pre-authenticated) | ||
| 31 | + | ||
| 32 | +Page objects are attached via `testFixture.ts`: | ||
| 33 | +```typescript | ||
| 34 | +const { fullTest } = require('./fixtures'); | ||
| 35 | +fullTest('my test', async ({ customerPage, authenticatedPage }) => { | ||
| 36 | + // customerPage is attached to authenticatedPage | ||
| 37 | +}); | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +Tests using `fullTest` must also set `storageState: 'auth.json'`: | ||
| 41 | +```typescript | ||
| 42 | +test.use({ storageState: 'auth.json' }); | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +### Page Object Pattern | ||
| 46 | + | ||
| 47 | +All page objects extend `BasePage` (`pages/basePage.ts`) which provides: | ||
| 48 | +- `navigate(path)` - Navigate using BASE_URL/#{path} | ||
| 49 | +- `click/fill/selectOptionByText` - Basic interactions | ||
| 50 | +- `waitForVisible/expectVisible/expectText` - Assertions | ||
| 51 | +- `takeScreenshot(name)` - Saves to `screenshots/` directory | ||
| 52 | +- `attachScreenshot(testInfo, name)` - Attaches to Allure report | ||
| 53 | +- `fillLicensePlate(plate)` - Handles custom license plate keyboard input | ||
| 54 | +- `uploadImage(path)` - Handles file upload dialogs | ||
| 55 | +- `selectRegion(province, city, district)` - Handles region picker dialogs | ||
| 56 | + | ||
| 57 | +### Test Organization | ||
| 58 | + | ||
| 59 | +- `tests/*.spec.ts` - Test specs | ||
| 60 | +- `pages/*.ts` - Page object classes | ||
| 61 | +- `fixtures/*.ts` - Test fixtures and authentication | ||
| 62 | +- `utils/dataGenerator.ts` - Test data generation utilities | ||
| 63 | +- `auth.json` - Saved authentication state (git-ignored) | ||
| 64 | + | ||
| 65 | +### Test Style | ||
| 66 | + | ||
| 67 | +Tests use Allure step reporting and screenshot attachments: | ||
| 68 | +```typescript | ||
| 69 | +await allure.step('Step description', async () => { | ||
| 70 | + await customerPage.createCustomer(info); | ||
| 71 | + await customerPage.attachScreenshot(testInfo, 'screenshot name'); | ||
| 72 | +}); | ||
| 73 | +``` | ||
| 74 | + | ||
| 75 | +Serial execution is used per test file to avoid interference: | ||
| 76 | +```typescript | ||
| 77 | +test.describe.configure({ mode: 'serial' }); | ||
| 78 | +``` | ||
| 79 | + | ||
| 80 | +## Environment | ||
| 81 | + | ||
| 82 | +Configure in `.env` (copy from `.env.example` if exists): | ||
| 83 | +- `BASE_URL` - Application URL (e.g., `http://localhost:8080`) | ||
| 84 | + | ||
| 85 | +## Key Files | ||
| 86 | + | ||
| 87 | +- `playwright.config.ts` - Test configuration, reporters (Allure HTML) | ||
| 88 | +- `fixtures/authFixture.ts` - Authentication via storageState | ||
| 89 | +- `fixtures/testFixture.ts` - Page object fixtures and fullTest | ||
| 90 | +- `pages/basePage.ts` - Base class with common UI interaction methods | ||
| 91 | +- `utils/dataGenerator.ts` - Functions for generating test data (names, phones, IDs, etc.) |
fixtures/testFixture.ts
| @@ -4,6 +4,8 @@ import { ProductPage } from '../pages/productPage'; | @@ -4,6 +4,8 @@ import { ProductPage } from '../pages/productPage'; | ||
| 4 | import { SalePage } from '../pages/salePage'; | 4 | import { SalePage } from '../pages/salePage'; |
| 5 | import { ConsignmentPage } from '../pages/consignmentPage'; | 5 | import { ConsignmentPage } from '../pages/consignmentPage'; |
| 6 | import { CustomerPage } from '../pages/customerPage'; | 6 | import { CustomerPage } from '../pages/customerPage'; |
| 7 | +import { SupplierPage } from '../pages/supplierPage'; | ||
| 8 | +import { SupplierGroupingPage } from '../pages/supplierGroupingPage'; | ||
| 7 | 9 | ||
| 8 | /** | 10 | /** |
| 9 | * 页面对象夹具类型定义 | 11 | * 页面对象夹具类型定义 |
| @@ -33,6 +35,16 @@ export type PageFixtures = { | @@ -33,6 +35,16 @@ export type PageFixtures = { | ||
| 33 | * 客户管理页面 | 35 | * 客户管理页面 |
| 34 | */ | 36 | */ |
| 35 | customerPage: CustomerPage; | 37 | customerPage: CustomerPage; |
| 38 | + | ||
| 39 | + /** | ||
| 40 | + * 供应商管理页面 | ||
| 41 | + */ | ||
| 42 | + supplierPage: SupplierPage; | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 供应商分组页面 | ||
| 46 | + */ | ||
| 47 | + supplierGroupingPage: SupplierGroupingPage; | ||
| 36 | }; | 48 | }; |
| 37 | 49 | ||
| 38 | /** | 50 | /** |
| @@ -63,6 +75,16 @@ export const test = base.extend<PageFixtures>({ | @@ -63,6 +75,16 @@ export const test = base.extend<PageFixtures>({ | ||
| 63 | customerPage: async ({ page }, use) => { | 75 | customerPage: async ({ page }, use) => { |
| 64 | await use(new CustomerPage(page)); | 76 | await use(new CustomerPage(page)); |
| 65 | }, | 77 | }, |
| 78 | + | ||
| 79 | + // 供应商管理页面 | ||
| 80 | + supplierPage: async ({ page }, use) => { | ||
| 81 | + await use(new SupplierPage(page)); | ||
| 82 | + }, | ||
| 83 | + | ||
| 84 | + // 供应商分组页面 | ||
| 85 | + supplierGroupingPage: async ({ page }, use) => { | ||
| 86 | + await use(new SupplierGroupingPage(page)); | ||
| 87 | + }, | ||
| 66 | }); | 88 | }); |
| 67 | 89 | ||
| 68 | /** | 90 | /** |
| @@ -100,4 +122,12 @@ export const fullTest = authTest.extend<PageFixtures>({ | @@ -100,4 +122,12 @@ export const fullTest = authTest.extend<PageFixtures>({ | ||
| 100 | await use(new CustomerPage(authenticatedPage)); | 122 | await use(new CustomerPage(authenticatedPage)); |
| 101 | }, | 123 | }, |
| 102 | 124 | ||
| 125 | + supplierPage: async ({authenticatedPage }, use) => { | ||
| 126 | + await use(new SupplierPage(authenticatedPage)); | ||
| 127 | + }, | ||
| 128 | + | ||
| 129 | + supplierGroupingPage: async ({authenticatedPage }, use) => { | ||
| 130 | + await use(new SupplierGroupingPage(authenticatedPage)); | ||
| 131 | + }, | ||
| 132 | + | ||
| 103 | }); | 133 | }); |
| 104 | \ No newline at end of file | 134 | \ No newline at end of file |
pages/supplierGroupingPage.ts
0 → 100644
| 1 | +import { Page, Locator } from '@playwright/test'; | ||
| 2 | +import { BasePage } from './basePage'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 供应商分组选择器 | ||
| 6 | + */ | ||
| 7 | +const selectors = { | ||
| 8 | + // 导航 | ||
| 9 | + moreMenu: 'text=更多 >', | ||
| 10 | + supplierGroupingMenu: 'text=供应商分组', | ||
| 11 | + addButton: 'text=新建', | ||
| 12 | + | ||
| 13 | + // 表单字段 | ||
| 14 | + groupNameInput: 'uni-input:filter([placeholder*="分组名称"]) input', | ||
| 15 | + remarkInput: 'uni-input:filter([placeholder*="备注"]) input', | ||
| 16 | + | ||
| 17 | + // 操作按钮 | ||
| 18 | + confirmButton: 'text=确定', | ||
| 19 | + saveButton: 'text=确定', | ||
| 20 | + | ||
| 21 | + // 供应商分组列表 | ||
| 22 | + supplierGroupingList: '.supplier-grouping-list', | ||
| 23 | + supplierGroupingItem: '.supplier-grouping-list .item', | ||
| 24 | +}; | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * 供应商分组页面类 | ||
| 28 | + * 处理供应商分组相关操作 | ||
| 29 | + */ | ||
| 30 | +export class SupplierGroupingPage extends BasePage { | ||
| 31 | + // 导航定位器 | ||
| 32 | + readonly moreMenu!: Locator; | ||
| 33 | + readonly supplierGroupingMenu: Locator; | ||
| 34 | + readonly addButton: Locator; | ||
| 35 | + | ||
| 36 | + // 表单字段 | ||
| 37 | + readonly groupNameInput: Locator; | ||
| 38 | + readonly remarkInput: Locator; | ||
| 39 | + | ||
| 40 | + // 操作按钮 | ||
| 41 | + readonly confirmButton: Locator; | ||
| 42 | + readonly saveButton: Locator; | ||
| 43 | + | ||
| 44 | + constructor(page: Page) { | ||
| 45 | + super(page); | ||
| 46 | + | ||
| 47 | + // 导航 | ||
| 48 | + this.moreMenu = page.getByText('更多 >'); | ||
| 49 | + this.supplierGroupingMenu = page.getByText('供应商分组').first(); | ||
| 50 | + this.addButton = page.getByText('新建'); | ||
| 51 | + | ||
| 52 | + // 表单字段 | ||
| 53 | + this.groupNameInput = page.locator('uni-input').filter({ hasText: '请输入分组名称' }).getByRole('textbox'); | ||
| 54 | + this.remarkInput = page.locator('uni-input').filter({ hasText: '请输入备注' }).getByRole('textbox'); | ||
| 55 | + | ||
| 56 | + // 操作按钮 | ||
| 57 | + this.confirmButton = page.getByText('确定'); | ||
| 58 | + this.saveButton = page.getByText('确定'); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + /** | ||
| 62 | + * 打开更多菜单 | ||
| 63 | + */ | ||
| 64 | + async openMoreMenu(): Promise<void> { | ||
| 65 | + await this.moreMenu.click(); | ||
| 66 | + await this.wait(500); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + /** | ||
| 70 | + * 打开供应商分组页面 | ||
| 71 | + */ | ||
| 72 | + async openSupplierGrouping(): Promise<void> { | ||
| 73 | + await this.supplierGroupingMenu.click({ force: true }); | ||
| 74 | + await this.waitForPageLoad(); | ||
| 75 | + await this.wait(500); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + /** | ||
| 79 | + * 点击新建分组按钮 | ||
| 80 | + */ | ||
| 81 | + async clickAddButton(): Promise<void> { | ||
| 82 | + await this.addButton.click(); | ||
| 83 | + await this.waitForPageLoad(); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 填写分组名称 | ||
| 88 | + * @param name 分组名称 | ||
| 89 | + */ | ||
| 90 | + async fillGroupName(name: string): Promise<void> { | ||
| 91 | + await this.groupNameInput.click(); | ||
| 92 | + await this.groupNameInput.fill(name); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * 填写备注 | ||
| 97 | + * @param remark 备注内容 | ||
| 98 | + */ | ||
| 99 | + async fillRemark(remark: string): Promise<void> { | ||
| 100 | + await this.remarkInput.click(); | ||
| 101 | + await this.remarkInput.fill(remark); | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 保存分组 | ||
| 106 | + */ | ||
| 107 | + async saveGrouping(): Promise<void> { | ||
| 108 | + await this.saveButton.click(); | ||
| 109 | + await this.waitForPageLoad(); | ||
| 110 | + await this.wait(500); | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + /** | ||
| 114 | + * 搜索分组 | ||
| 115 | + * @param groupName 分组名称 | ||
| 116 | + */ | ||
| 117 | + async searchGrouping(groupName: string): Promise<void> { | ||
| 118 | + await this.page.getByRole('textbox').click(); | ||
| 119 | + await this.wait(300); | ||
| 120 | + await this.page.getByRole('textbox').fill(groupName); | ||
| 121 | + await this.wait(1500); | ||
| 122 | + await this.page.getByRole('textbox').press('Enter'); | ||
| 123 | + await this.wait(1000); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + /** | ||
| 127 | + * 验证分组是否创建成功 | ||
| 128 | + * @param groupName 分组名称 | ||
| 129 | + */ | ||
| 130 | + async verifyGroupingCreated(groupName: string): Promise<boolean> { | ||
| 131 | + try { | ||
| 132 | + await this.navigateToSupplierGrouping(); | ||
| 133 | + await this.searchGrouping(groupName); | ||
| 134 | + await this.page.waitForSelector(`uni-view:has-text("${groupName}")`, { timeout: 10000 }); | ||
| 135 | + return true; | ||
| 136 | + } catch { | ||
| 137 | + return false; | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + /** | ||
| 142 | + * 进入供应商分组页面(完整流程) | ||
| 143 | + */ | ||
| 144 | + async navigateToSupplierGrouping(): Promise<void> { | ||
| 145 | + await this.navigate('/'); | ||
| 146 | + await this.openMoreMenu(); | ||
| 147 | + await this.openSupplierGrouping(); | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + /** | ||
| 151 | + * 点击编辑分组按钮 | ||
| 152 | + */ | ||
| 153 | + async clickEditButton(): Promise<void> { | ||
| 154 | + await this.page.getByText('编辑').click(); | ||
| 155 | + await this.waitForPageLoad(); | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + /** | ||
| 159 | + * 修改分组 | ||
| 160 | + * @param groupName 新的分组名称 | ||
| 161 | + * @param remark 新的备注 | ||
| 162 | + */ | ||
| 163 | + async updateGrouping(groupName: string, remark: string): Promise<void> { | ||
| 164 | + await this.fillGroupName(groupName); | ||
| 165 | + await this.fillRemark(remark); | ||
| 166 | + await this.saveGrouping(); | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + /** | ||
| 170 | + * 验证分组是否修改成功 | ||
| 171 | + * @param groupName 分组名称 | ||
| 172 | + */ | ||
| 173 | + async verifyGroupingUpdated(groupName: string): Promise<boolean> { | ||
| 174 | + try { | ||
| 175 | + await this.navigateToSupplierGrouping(); | ||
| 176 | + await this.searchGrouping(groupName); | ||
| 177 | + await this.page.waitForSelector(`uni-view:has-text("${groupName}")`, { timeout: 10000 }); | ||
| 178 | + return true; | ||
| 179 | + } catch { | ||
| 180 | + return false; | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + /** | ||
| 185 | + * 点击删除分组按钮 | ||
| 186 | + */ | ||
| 187 | + async clickDeleteButton(): Promise<void> { | ||
| 188 | + await this.page.getByText('删除', { exact: true }).click(); | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + /** | ||
| 192 | + * 点击分组项 | ||
| 193 | + * @param groupName 分组名称 | ||
| 194 | + */ | ||
| 195 | + async clickGroupingItem(groupName: string): Promise<void> { | ||
| 196 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${groupName}$`) }).first().click(); | ||
| 197 | + await this.waitForPageLoad(); | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + /** | ||
| 201 | + * 确认删除分组 | ||
| 202 | + */ | ||
| 203 | + async confirmDelete(): Promise<void> { | ||
| 204 | + await this.page.getByText('确定', { exact: true }).click(); | ||
| 205 | + await this.waitForPageLoad(); | ||
| 206 | + await this.wait(500); | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + /** | ||
| 210 | + * 验证分组是否删除成功 | ||
| 211 | + * @param groupName 分组名称 | ||
| 212 | + */ | ||
| 213 | + async verifyGroupingDeleted(groupName: string): Promise<boolean> { | ||
| 214 | + try { | ||
| 215 | + await this.navigateToSupplierGrouping(); | ||
| 216 | + await this.searchGrouping(groupName); | ||
| 217 | + // 删除成功后,列表会显示"没有数据哦~" | ||
| 218 | + await this.page.waitForSelector(`uni-view:has-text("No data")`, { timeout: 10000 }); | ||
| 219 | + return true; | ||
| 220 | + } catch { | ||
| 221 | + return false; | ||
| 222 | + } | ||
| 223 | + } | ||
| 224 | +} |
pages/supplierPage.ts
0 → 100644
| 1 | +import { Page, Locator, expect } from '@playwright/test'; | ||
| 2 | +import { BasePage } from './basePage'; | ||
| 3 | +import { generateSupplierName, generatePhoneNumber, generateChineseName } from '../utils/dataGenerator'; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 供应商信息接口 | ||
| 7 | + */ | ||
| 8 | +export interface SupplierInfo { | ||
| 9 | + name: string; | ||
| 10 | + managerName: string; | ||
| 11 | + phone: string; | ||
| 12 | + remark?: string; | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * 供应商选项接口 | ||
| 17 | + */ | ||
| 18 | +export interface SupplierOptions { | ||
| 19 | + enableParkCard?: boolean; | ||
| 20 | + parkCardNumber?: string; | ||
| 21 | + parkCardChannel?: string; | ||
| 22 | +} | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * 供应商管理页面选择器 | ||
| 26 | + */ | ||
| 27 | +const selectors = { | ||
| 28 | + // 导航 | ||
| 29 | + moreMenu: 'text=更多 >', | ||
| 30 | + supplierMenu: 'text=供应商管理', | ||
| 31 | + addSupplierButton: 'text=新建供应商', | ||
| 32 | + | ||
| 33 | + // 表单字段 | ||
| 34 | + supplierNameInput: 'uni-input:has-text("请输入供应商名称") input', | ||
| 35 | + managerNameInput: 'uni-input:has-text("请输入负责人名称") input', | ||
| 36 | + phoneInput: 'spinbutton', | ||
| 37 | + | ||
| 38 | + // 供应商分组选择器 | ||
| 39 | + groupPicker: '.nut-input__mask', | ||
| 40 | + groupOption: 'text=普通供应商', | ||
| 41 | + | ||
| 42 | + // 备注 | ||
| 43 | + remarkInput: 'uni-input:has-text("请输入备注") input', | ||
| 44 | + | ||
| 45 | + // 园区卡 | ||
| 46 | + parkCardSearchInput: 'uni-view:nth-child(2) > .nut-searchbar__search-input', | ||
| 47 | + queryButton: 'text=查询', | ||
| 48 | + bindButton: 'uni-button', | ||
| 49 | + | ||
| 50 | + // 操作按钮 | ||
| 51 | + confirmButton: 'text=确定', | ||
| 52 | + saveButton: 'text=确定', | ||
| 53 | + | ||
| 54 | + // 供应商列表 | ||
| 55 | + supplierList: '.supplier-list', | ||
| 56 | + supplierItem: '.supplier-list .item', | ||
| 57 | + supplierItemClick: '.supplier-list .item-click', | ||
| 58 | +}; | ||
| 59 | + | ||
| 60 | +/** | ||
| 61 | + * 供应商管理页面类 | ||
| 62 | + * 处理供应商管理相关操作 | ||
| 63 | + */ | ||
| 64 | +export class SupplierPage extends BasePage { | ||
| 65 | + // 导航定位器 | ||
| 66 | + readonly moreMenu!: Locator; | ||
| 67 | + readonly supplierMenu: Locator; | ||
| 68 | + readonly addSupplierButton: Locator; | ||
| 69 | + | ||
| 70 | + // 表单字段 | ||
| 71 | + readonly supplierNameInput: Locator; | ||
| 72 | + readonly managerNameInput: Locator; | ||
| 73 | + readonly phoneInput: Locator; | ||
| 74 | + | ||
| 75 | + // 供应商分组 | ||
| 76 | + readonly groupPicker: Locator; | ||
| 77 | + readonly groupOption: Locator; | ||
| 78 | + | ||
| 79 | + // 备注 | ||
| 80 | + readonly remarkInput: Locator; | ||
| 81 | + | ||
| 82 | + // 园区卡 | ||
| 83 | + readonly parkCardSearchInput: Locator; | ||
| 84 | + readonly queryButton: Locator; | ||
| 85 | + readonly bindButton: Locator; | ||
| 86 | + | ||
| 87 | + // 操作按钮 | ||
| 88 | + readonly confirmButton: Locator; | ||
| 89 | + readonly saveButton: Locator; | ||
| 90 | + | ||
| 91 | + constructor(page: Page) { | ||
| 92 | + super(page); | ||
| 93 | + | ||
| 94 | + // 导航 | ||
| 95 | + this.moreMenu = page.getByText('更多 >'); | ||
| 96 | + this.supplierMenu = page.getByText('供应商管理').first(); | ||
| 97 | + this.addSupplierButton = page.getByText('新建供应商'); | ||
| 98 | + | ||
| 99 | + // 表单字段 | ||
| 100 | + this.supplierNameInput = page.locator('uni-input').filter({ hasText: '请输入供应商名称' }).getByRole('textbox'); | ||
| 101 | + this.managerNameInput = page.locator('uni-input').filter({ hasText: '请输入负责人名称' }).getByRole('textbox'); | ||
| 102 | + this.phoneInput = page.getByRole('spinbutton'); | ||
| 103 | + this.remarkInput = page.locator('uni-input').filter({ hasText: '请输入备注' }).getByRole('textbox'); | ||
| 104 | + | ||
| 105 | + // 供应商分组 | ||
| 106 | + this.groupPicker = page.locator('.nut-input__mask').first(); | ||
| 107 | + this.groupOption = page.locator('.select-item'); | ||
| 108 | + | ||
| 109 | + // 园区卡 | ||
| 110 | + this.parkCardSearchInput = page.locator('uni-view:nth-child(2) > .nut-searchbar__search-input > .nut-searchbar__input-inner > .nut-searchbar__input-form > span > .nut-searchbar__input-bar > .uni-input-wrapper > .uni-input-input'); | ||
| 111 | + this.queryButton = page.getByText('查询', { exact: true }); | ||
| 112 | + this.bindButton = page.locator('uni-button').filter({ hasText: '绑定' }); | ||
| 113 | + | ||
| 114 | + // 操作按钮 | ||
| 115 | + this.confirmButton = page.getByText('确定'); | ||
| 116 | + this.saveButton = page.getByText('确定'); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + /** | ||
| 120 | + * 导航到首页并等待加载 | ||
| 121 | + */ | ||
| 122 | + async gotoHome(): Promise<void> { | ||
| 123 | + await this.navigate('/'); | ||
| 124 | + await this.waitForPageLoad(); | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + /** | ||
| 128 | + * 打开更多菜单 | ||
| 129 | + */ | ||
| 130 | + async openMoreMenu(): Promise<void> { | ||
| 131 | + await this.moreMenu.click(); | ||
| 132 | + await this.wait(500); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + /** | ||
| 136 | + * 打开供应商管理页面 | ||
| 137 | + */ | ||
| 138 | + async openSupplierManagement(): Promise<void> { | ||
| 139 | + await this.supplierMenu.click({ force: true }); | ||
| 140 | + await this.waitForPageLoad(); | ||
| 141 | + await this.wait(500); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + /** | ||
| 145 | + * 点击新建供应商按钮 | ||
| 146 | + */ | ||
| 147 | + async clickAddSupplier(): Promise<void> { | ||
| 148 | + await this.addSupplierButton.click(); | ||
| 149 | + await this.waitForPageLoad(); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + /** | ||
| 153 | + * 点击编辑按钮 | ||
| 154 | + */ | ||
| 155 | + async clickEditButton(): Promise<void> { | ||
| 156 | + await this.page.getByText('编辑').click(); | ||
| 157 | + await this.waitForPageLoad(); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + /** | ||
| 161 | + * 点击确定/保存按钮 | ||
| 162 | + */ | ||
| 163 | + async clickConfirmButton(): Promise<void> { | ||
| 164 | + await this.confirmButton.click(); | ||
| 165 | + await this.waitForPageLoad(); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + /** | ||
| 169 | + * 填写表单字段(通用方法) | ||
| 170 | + * @param locator 表单字段定位器 | ||
| 171 | + * @param value 填写值 | ||
| 172 | + */ | ||
| 173 | + async fillFormField(locator: Locator, value: string): Promise<void> { | ||
| 174 | + await locator.click(); | ||
| 175 | + await locator.fill(value); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + /** | ||
| 179 | + * 清除并填写表单字段(通用方法) | ||
| 180 | + * @param locator 表单字段定位器 | ||
| 181 | + * @param value 填写值 | ||
| 182 | + */ | ||
| 183 | + async clearAndFillFormField(locator: Locator, value: string): Promise<void> { | ||
| 184 | + await locator.click(); | ||
| 185 | + await locator.fill(''); | ||
| 186 | + await locator.fill(value); | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + /** | ||
| 190 | + * 点击选择器弹窗(通用方法,用于供应商分组、园区卡等) | ||
| 191 | + * @param index 选择器索引(0=第一个,1=第二个) | ||
| 192 | + */ | ||
| 193 | + async clickPickerSelector(index: number = 0): Promise<void> { | ||
| 194 | + await this.page.locator('.nut-form-item__body__slots .nut-input__mask').nth(index).click(); | ||
| 195 | + await this.wait(500); | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + /** | ||
| 199 | + * 选择供应商类型(必选字段) | ||
| 200 | + * @param type 供应商类型(默认"代卖") | ||
| 201 | + */ | ||
| 202 | + async selectSupplierType(type: string = '代卖'): Promise<void> { | ||
| 203 | + await this.page.locator('.nut-form-item__body__slots > .nut-checkbox-group > uni-view').first().click(); | ||
| 204 | + await this.wait(500); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + /** | ||
| 208 | + * 选择供应商分组 | ||
| 209 | + */ | ||
| 210 | + async selectGroup(): Promise<void> { | ||
| 211 | + await this.groupPicker.first().click(); | ||
| 212 | + await this.wait(500); | ||
| 213 | + await this.groupOption.click(); | ||
| 214 | + await this.wait(500); | ||
| 215 | + } | ||
| 216 | + | ||
| 217 | + /** | ||
| 218 | + * 选择园区卡渠道 | ||
| 219 | + * @param channel 渠道名称 | ||
| 220 | + */ | ||
| 221 | + async selectParkCardChannel(channel: string = '地利'): Promise<void> { | ||
| 222 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${channel}$`) }).first().click(); | ||
| 223 | + await this.wait(500); | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + /** | ||
| 227 | + * 搜索园区卡 | ||
| 228 | + * @param cardNumber 园区卡号 | ||
| 229 | + */ | ||
| 230 | + async searchParkCard(cardNumber: string): Promise<void> { | ||
| 231 | + await this.parkCardSearchInput.click(); | ||
| 232 | + await this.parkCardSearchInput.fill(cardNumber); | ||
| 233 | + await this.queryButton.click(); | ||
| 234 | + await this.wait(500); | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + /** | ||
| 238 | + * 绑定园区卡 | ||
| 239 | + */ | ||
| 240 | + async bindParkCard(): Promise<void> { | ||
| 241 | + await this.bindButton.click(); | ||
| 242 | + await this.waitForPageLoad(); | ||
| 243 | + await this.wait(500); | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + /** | ||
| 247 | + * 选择园区卡(完整流程) | ||
| 248 | + * @param cardNumber 园区卡号 | ||
| 249 | + * @param channel 渠道名称(可选) | ||
| 250 | + */ | ||
| 251 | + async selectParkCard(cardNumber: string, channel?: string): Promise<void> { | ||
| 252 | + // 点击园区卡输入框(第二个,因为第一个是供应商分组) | ||
| 253 | + await this.clickPickerSelector(1); | ||
| 254 | + | ||
| 255 | + // 选择渠道(如果指定) | ||
| 256 | + if (channel) { | ||
| 257 | + await this.selectParkCardChannel(channel); | ||
| 258 | + } | ||
| 259 | + | ||
| 260 | + // 搜索并绑定 | ||
| 261 | + await this.searchParkCard(cardNumber); | ||
| 262 | + await this.bindParkCard(); | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + /** | ||
| 266 | + * 点击供应商列表项 | ||
| 267 | + * @param index 列表索引(0=第一个.item,1=第二个.item) | ||
| 268 | + */ | ||
| 269 | + async clickSupplierItemByIndex(index: number = 0): Promise<void> { | ||
| 270 | + await this.page.locator(selectors.supplierItem).nth(index).click(); | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + /** | ||
| 274 | + * 搜索供应商 | ||
| 275 | + * @param supplierName 供应商名称 | ||
| 276 | + */ | ||
| 277 | + async searchSupplier(supplierName: string): Promise<void> { | ||
| 278 | + await this.page.getByRole('textbox').click(); | ||
| 279 | + const input = this.page.locator('uni-input').filter({ hasText: '供应商名称/联系电话' }).getByRole('textbox'); | ||
| 280 | + await input.fill(supplierName); | ||
| 281 | + await this.page.waitForTimeout(1500); | ||
| 282 | + await input.press('Enter'); | ||
| 283 | + await this.page.waitForTimeout(1000); | ||
| 284 | + } | ||
| 285 | + | ||
| 286 | + /** | ||
| 287 | + * 进入供应商管理页面(完整流程) | ||
| 288 | + */ | ||
| 289 | + async navigateToSupplierManagement(): Promise<void> { | ||
| 290 | + await this.gotoHome(); | ||
| 291 | + await this.openMoreMenu(); | ||
| 292 | + await this.openSupplierManagement(); | ||
| 293 | + } | ||
| 294 | + | ||
| 295 | + /** | ||
| 296 | + * 验证供应商是否创建成功 | ||
| 297 | + * @param supplierName 供应商名称 | ||
| 298 | + */ | ||
| 299 | + async verifySupplierCreated(supplierName: string): Promise<boolean> { | ||
| 300 | + try { | ||
| 301 | + await this.navigateToSupplierManagement(); | ||
| 302 | + await this.searchSupplier(supplierName); | ||
| 303 | + await this.page.waitForSelector(`uni-view:has-text("${supplierName}")`, { timeout: 10000 }); | ||
| 304 | + return true; | ||
| 305 | + } catch { | ||
| 306 | + return false; | ||
| 307 | + } | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + /** | ||
| 311 | + * 点击删除按钮 | ||
| 312 | + */ | ||
| 313 | + async clickDeleteButton(): Promise<void> { | ||
| 314 | + await this.page.getByText('删除').click(); | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + /** | ||
| 318 | + * 确认删除 | ||
| 319 | + */ | ||
| 320 | + async confirmDelete(): Promise<void> { | ||
| 321 | + await this.page.getByText('确定', { exact: true }).click(); | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + /** | ||
| 325 | + * 点击录入欠款按钮 | ||
| 326 | + */ | ||
| 327 | + async clickRecordDebt(): Promise<void> { | ||
| 328 | + await this.page.getByText('录入欠款').click(); | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + /** | ||
| 332 | + * 点击返回按钮 | ||
| 333 | + */ | ||
| 334 | + async clickBackButton(): Promise<void> { | ||
| 335 | + await this.page.locator('.uni-icons.uniui-left').click(); | ||
| 336 | + await this.wait(300); | ||
| 337 | + } | ||
| 338 | + | ||
| 339 | + /** | ||
| 340 | + * 选择欠款方向 | ||
| 341 | + * @param direction 欠款方向('我方欠供应商' 或 '供应商欠我方') | ||
| 342 | + */ | ||
| 343 | + async selectDebtDirection(direction: 'oweSupplier' | 'supplierOwe'): Promise<void> { | ||
| 344 | + const text = direction === 'oweSupplier' ? '我方欠供应商' : '供应商欠我方'; | ||
| 345 | + await this.page.getByText(text, { exact: true }).click(); | ||
| 346 | + } | ||
| 347 | + | ||
| 348 | + /** | ||
| 349 | + * 选择赊欠日期 | ||
| 350 | + * @param day 日期(1-31),不传则选择今日 | ||
| 351 | + */ | ||
| 352 | + async selectDebtDate(day?: number): Promise<void> { | ||
| 353 | + await this.page.getByText('赊欠日期*请选择赊欠日期').click(); | ||
| 354 | + await this.wait(300); | ||
| 355 | + | ||
| 356 | + if (day) { | ||
| 357 | + await this.page.getByText(`${day}日`).click(); | ||
| 358 | + } else { | ||
| 359 | + const today = new Date().getDate(); | ||
| 360 | + await this.page.getByText(`${today}today`).click(); | ||
| 361 | + } | ||
| 362 | + await this.wait(300); | ||
| 363 | + } | ||
| 364 | + | ||
| 365 | + /** | ||
| 366 | + * 填写赊欠金额 | ||
| 367 | + * @param amount 金额 | ||
| 368 | + */ | ||
| 369 | + async fillDebtAmount(amount: string): Promise<void> { | ||
| 370 | + const amountMask = this.page.locator('uni-view:nth-child(6) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask'); | ||
| 371 | + await amountMask.click(); | ||
| 372 | + await this.wait(300); | ||
| 373 | + | ||
| 374 | + await this.page.getByText('1', { exact: true }).click(); | ||
| 375 | + await this.page.getByText('0', { exact: true }).nth(0).click(); | ||
| 376 | + await this.page.getByText('0', { exact: true }).nth(0).click(); | ||
| 377 | + await this.page.getByText('完成').click(); | ||
| 378 | + } | ||
| 379 | + | ||
| 380 | + /** | ||
| 381 | + * 填写备注 | ||
| 382 | + * @param remark 备注内容 | ||
| 383 | + */ | ||
| 384 | + async fillRemark(remark: string): Promise<void> { | ||
| 385 | + const remarkInput = this.page.locator('uni-view:nth-child(7) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input'); | ||
| 386 | + await remarkInput.click(); | ||
| 387 | + await remarkInput.fill(remark); | ||
| 388 | + } | ||
| 389 | + | ||
| 390 | + /** | ||
| 391 | + * 上传欠款凭证 | ||
| 392 | + * @param imagePath 图片路径 | ||
| 393 | + */ | ||
| 394 | + async uploadDebtVoucher(imagePath: string): Promise<void> { | ||
| 395 | + await this.page.locator('body').setInputFiles(imagePath); | ||
| 396 | + } | ||
| 397 | + | ||
| 398 | + /** | ||
| 399 | + * 保存欠款记录 | ||
| 400 | + */ | ||
| 401 | + async saveDebt(): Promise<void> { | ||
| 402 | + await this.page.getByText('保存').click(); | ||
| 403 | + await this.waitForPageLoad(); | ||
| 404 | + } | ||
| 405 | + | ||
| 406 | + /** | ||
| 407 | + * 获取供应商名称(从详情页) | ||
| 408 | + */ | ||
| 409 | + async getSupplierName(): Promise<string> { | ||
| 410 | + // 供应商名称通常显示在页面顶部,可能是 "供应商名称" 标签后面 | ||
| 411 | + const nameElement = this.page.locator('uni-view').filter({ hasText: /^供应商名称/ }).locator('..').locator('uni-view').last(); | ||
| 412 | + const name = await nameElement.textContent(); | ||
| 413 | + return name || ''; | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + /** | ||
| 417 | + * 获取应付金额 | ||
| 418 | + * @returns 应付金额字符串,如 '¥ 221300.01' | ||
| 419 | + */ | ||
| 420 | + async getPayableAmount(): Promise<string> { | ||
| 421 | + const amountText = await this.page.getByText(/应付 ¥/).textContent(); | ||
| 422 | + return amountText || ''; | ||
| 423 | + } | ||
| 424 | + | ||
| 425 | + /** | ||
| 426 | + * 解析应付金额为数字 | ||
| 427 | + * @returns 应付金额数值 | ||
| 428 | + */ | ||
| 429 | + async parsePayableAmount(): Promise<number> { | ||
| 430 | + const amountText = await this.getPayableAmount(); | ||
| 431 | + const match = amountText.match(/¥ ?([\d.]+)/); | ||
| 432 | + return match ? parseFloat(match[1]) : 0; | ||
| 433 | + } | ||
| 434 | + | ||
| 435 | + /** | ||
| 436 | + * 验证欠款录入成功(检查应付金额是否增加) | ||
| 437 | + * @param originalAmount 原始应付金额 | ||
| 438 | + * @param addedAmount 新增金额 | ||
| 439 | + * @returns 是否验证成功 | ||
| 440 | + */ | ||
| 441 | + async verifyDebtRecorded(originalAmount: number, addedAmount: number): Promise<boolean> { | ||
| 442 | + try { | ||
| 443 | + const currentAmount = await this.parsePayableAmount(); | ||
| 444 | + // 允许小幅误差(由于金额格式或四舍五入) | ||
| 445 | + const expectedAmount = originalAmount + addedAmount; | ||
| 446 | + return Math.abs(currentAmount - expectedAmount) < 0.01; | ||
| 447 | + } catch { | ||
| 448 | + return false; | ||
| 449 | + } | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + /** | ||
| 453 | + * 点击供应商的金额显示区域 | ||
| 454 | + * @param index 供应商索引 | ||
| 455 | + */ | ||
| 456 | + async clickSupplierAmount(index: number): Promise<void> { | ||
| 457 | + await this.page.getByText('¥').nth(index).click(); | ||
| 458 | + } | ||
| 459 | + | ||
| 460 | + /** | ||
| 461 | + * 获取供应商列表中所有供应商的金额信息 | ||
| 462 | + * @returns 金额数组 | ||
| 463 | + */ | ||
| 464 | + async getSupplierAmounts(): Promise<number[]> { | ||
| 465 | + const yuanElements = await this.page.getByText('¥').all(); | ||
| 466 | + const amounts: number[] = []; | ||
| 467 | + | ||
| 468 | + for (const element of yuanElements) { | ||
| 469 | + const text = await element.textContent(); | ||
| 470 | + const match = text?.match(/¥(\d+)/); | ||
| 471 | + if (match) { | ||
| 472 | + amounts.push(parseInt(match[1], 10)); | ||
| 473 | + } | ||
| 474 | + } | ||
| 475 | + | ||
| 476 | + return amounts; | ||
| 477 | + } | ||
| 478 | + | ||
| 479 | + /** | ||
| 480 | + * 查找符合条件的供应商(所有金额都是0) | ||
| 481 | + * @returns 是否找到符合条件的供应商 | ||
| 482 | + */ | ||
| 483 | + async clickSupplierWithZeroAmounts(): Promise<boolean> { | ||
| 484 | + // 获取所有供应商项 | ||
| 485 | + const supplierItems = await this.page.locator(selectors.supplierItem).all(); | ||
| 486 | + | ||
| 487 | + for (let i = 0; i < supplierItems.length; i++) { | ||
| 488 | + // 点击供应商列表项 | ||
| 489 | + await this.clickSupplierItemByIndex(i); | ||
| 490 | + await this.wait(300); | ||
| 491 | + | ||
| 492 | + // 获取所有金额文本,检查是否都为'¥ 0' | ||
| 493 | + const yuanTexts = await this.page.locator('text=¥').allTextContents(); | ||
| 494 | + | ||
| 495 | + // 检查所有金额是否都是'¥ 0' | ||
| 496 | + const allZero = yuanTexts.every(text => { | ||
| 497 | + const trimmed = text.trim(); | ||
| 498 | + return trimmed === '¥ 0.00' || trimmed === '¥0.00'; | ||
| 499 | + }); | ||
| 500 | + | ||
| 501 | + // 如果全为0,返回true(保持在当前页面继续删除流程) | ||
| 502 | + if (allZero && yuanTexts.length > 0) { | ||
| 503 | + return true; | ||
| 504 | + } | ||
| 505 | + | ||
| 506 | + // 如果不全为0,继续尝试下一个供应商(不返回页面,直接点击下一个) | ||
| 507 | + } | ||
| 508 | + | ||
| 509 | + return false; | ||
| 510 | + } | ||
| 511 | + | ||
| 512 | + /** | ||
| 513 | + * 生成随机供应商信息 | ||
| 514 | + * @param options 可选配置 | ||
| 515 | + */ | ||
| 516 | + async generateRandomSupplierInfo(options?: { | ||
| 517 | + namePrefix?: string; | ||
| 518 | + managerName?: string; | ||
| 519 | + }): Promise<SupplierInfo> { | ||
| 520 | + const timestamp = Date.now().toString().slice(-6); | ||
| 521 | + | ||
| 522 | + const name = options?.namePrefix | ||
| 523 | + ? `${options.namePrefix}_${timestamp}` | ||
| 524 | + : generateSupplierName(); | ||
| 525 | + | ||
| 526 | + const managerName = options?.managerName || generateChineseName(); | ||
| 527 | + | ||
| 528 | + return { | ||
| 529 | + name, | ||
| 530 | + managerName, | ||
| 531 | + phone: generatePhoneNumber(), | ||
| 532 | + remark: `自动化测试_${timestamp}`, | ||
| 533 | + }; | ||
| 534 | + } | ||
| 535 | + | ||
| 536 | + | ||
| 537 | +} |
scripts/codegen-authenticated.ts
0 → 100644
| 1 | +/** | ||
| 2 | + * 启动已认证的 Playwright Codegen | ||
| 3 | + * | ||
| 4 | + * 使用方式: | ||
| 5 | + * 1. 先确保 auth.json 已存在(运行 npx playwright test login.setup.ts) | ||
| 6 | + * 2. 然后运行:npx ts-node scripts/codegen-authenticated.ts | ||
| 7 | + * | ||
| 8 | + * 或者直接使用命令: | ||
| 9 | + * npx playwright codegen --storage-state=auth.json $BASE_URL | ||
| 10 | + */ | ||
| 11 | + | ||
| 12 | +import { spawn } from 'child_process'; | ||
| 13 | +import path from 'path'; | ||
| 14 | +import fs from 'fs'; | ||
| 15 | +import dotenv from 'dotenv'; | ||
| 16 | + | ||
| 17 | +// 加载环境变量 | ||
| 18 | +dotenv.config({ path: path.resolve(__dirname, '../.env') }); | ||
| 19 | + | ||
| 20 | +const authFile = path.join(process.cwd(), 'auth.json'); | ||
| 21 | +const baseUrl = process.env.BASE_URL || 'http://localhost:8080'; | ||
| 22 | + | ||
| 23 | +console.log('='.repeat(50)); | ||
| 24 | +console.log('启动已认证的 Playwright Codegen'); | ||
| 25 | +console.log('='.repeat(50)); | ||
| 26 | +console.log(`认证文件: ${authFile}`); | ||
| 27 | +console.log(`目标地址: ${baseUrl}`); | ||
| 28 | +console.log(''); | ||
| 29 | + | ||
| 30 | +// 检查 auth.json 是否存在 | ||
| 31 | +if (!fs.existsSync(authFile)) { | ||
| 32 | + console.error('错误: auth.json 文件不存在!'); | ||
| 33 | + console.error('请先运行以下命令生成认证文件:'); | ||
| 34 | + console.error(' npx playwright test login.setup.ts'); | ||
| 35 | + console.log(''); | ||
| 36 | + console.error('如果环境变量未设置,还需要设置:'); | ||
| 37 | + console.error(' BASE_URL - 目标网站地址'); | ||
| 38 | + console.error(' TEST_USER_NAME - 登录后显示的用户名(用于验证登录成功)'); | ||
| 39 | + process.exit(1); | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +console.log('✓ auth.json 文件存在'); | ||
| 43 | +console.log(''); | ||
| 44 | + | ||
| 45 | +// 启动 codegen | ||
| 46 | +console.log('正在启动 Codegen...'); | ||
| 47 | +console.log(''); | ||
| 48 | + | ||
| 49 | +const isWindows = process.platform === 'win32'; | ||
| 50 | + | ||
| 51 | +// Playwright codegen 使用 --load-storage 来加载已保存的认证状态 | ||
| 52 | +const loadStorageArg = isWindows ? `--load-storage ${authFile}` : `--load-storage=${authFile}`; | ||
| 53 | + | ||
| 54 | +const codegen = spawn( | ||
| 55 | + 'npx', | ||
| 56 | + ['playwright', 'codegen', loadStorageArg, baseUrl], | ||
| 57 | + { | ||
| 58 | + stdio: 'inherit', | ||
| 59 | + shell: true, | ||
| 60 | + cwd: process.cwd() | ||
| 61 | + } | ||
| 62 | +); | ||
| 63 | + | ||
| 64 | +codegen.on('close', (code) => { | ||
| 65 | + if (code !== 0) { | ||
| 66 | + console.error(`Codegen 退出,代码: ${code}`); | ||
| 67 | + } | ||
| 68 | + process.exit(code ?? 0); | ||
| 69 | +}); | ||
| 70 | + | ||
| 71 | +codegen.on('error', (err) => { | ||
| 72 | + console.error('启动 Codegen 失败:', err); | ||
| 73 | + process.exit(1); | ||
| 74 | +}); | ||
| 75 | + | ||
| 76 | +// 处理 Ctrl+C | ||
| 77 | +process.on('SIGINT', () => { | ||
| 78 | + console.log('\n正在关闭 Codegen...'); | ||
| 79 | + codegen.kill('SIGINT'); | ||
| 80 | +}); |
tests/supplier.spec.ts
0 → 100644
| 1 | +import { test, expect } from '../fixtures'; | ||
| 2 | +import * as allure from 'allure-js-commons'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 供应商管理测试 | ||
| 6 | + */ | ||
| 7 | +test.describe('供应商管理', () => { | ||
| 8 | + // 使用已保存的认证状态 | ||
| 9 | + test.use({ storageState: 'auth.json' }); | ||
| 10 | + | ||
| 11 | + // 强制测试串行执行,避免并行测试之间的干扰 | ||
| 12 | + test.describe.configure({ mode: 'serial' }); | ||
| 13 | + | ||
| 14 | + test('新建供应商并绑定园区卡', async ({ supplierPage }, testInfo) => { | ||
| 15 | + await allure.epic('供应商管理'); | ||
| 16 | + await allure.feature('供应商信息'); | ||
| 17 | + await allure.story('新建供应商'); | ||
| 18 | + | ||
| 19 | + // 步骤1:生成随机供应商信息 | ||
| 20 | + const supplierInfo = await allure.step('生成随机供应商信息', async () => { | ||
| 21 | + const info = await supplierPage.generateRandomSupplierInfo(); | ||
| 22 | + | ||
| 23 | + console.log('供应商名称:', info.name); | ||
| 24 | + console.log('负责人:', info.managerName); | ||
| 25 | + console.log('手机号:', info.phone); | ||
| 26 | + console.log('备注:', info.remark); | ||
| 27 | + | ||
| 28 | + return info; | ||
| 29 | + }); | ||
| 30 | + | ||
| 31 | + // 步骤2:填写并提交供应商表单 | ||
| 32 | + await allure.step('填写并提交供应商表单', async () => { | ||
| 33 | + await supplierPage.navigateToSupplierManagement(); | ||
| 34 | + await supplierPage.clickAddSupplier(); | ||
| 35 | + await supplierPage.fillFormField(supplierPage.supplierNameInput, supplierInfo.name); | ||
| 36 | + await supplierPage.selectGroup(); | ||
| 37 | + await supplierPage.fillFormField(supplierPage.managerNameInput, supplierInfo.managerName); | ||
| 38 | + await supplierPage.fillFormField(supplierPage.phoneInput, supplierInfo.phone); | ||
| 39 | + await supplierPage.selectParkCard('888810046477', '地利'); | ||
| 40 | + await supplierPage.fillFormField(supplierPage.remarkInput, supplierInfo.remark || ''); | ||
| 41 | + await supplierPage.saveButton.click(); | ||
| 42 | + await supplierPage.attachScreenshot(testInfo, '新建供应商成功截图'); | ||
| 43 | + }); | ||
| 44 | + | ||
| 45 | + // 步骤3:验证供应商创建成功 | ||
| 46 | + await allure.step('验证供应商创建成功', async () => { | ||
| 47 | + const isCreated = await supplierPage.verifySupplierCreated(supplierInfo.name); | ||
| 48 | + expect(isCreated).toBeTruthy(); | ||
| 49 | + }); | ||
| 50 | + }); | ||
| 51 | + | ||
| 52 | + test('编辑供应商并绑定园区卡', async ({ supplierPage }, testInfo) => { | ||
| 53 | + await allure.epic('供应商管理'); | ||
| 54 | + await allure.feature('供应商信息'); | ||
| 55 | + await allure.story('编辑供应商'); | ||
| 56 | + | ||
| 57 | + // 步骤1:生成新的供应商信息 | ||
| 58 | + const supplierInfo = await allure.step('生成新的供应商信息', async () => { | ||
| 59 | + const namePrefixes = ['修改', '更新', '调整', '变更']; | ||
| 60 | + const namePrefix = namePrefixes[Math.floor(Math.random() * namePrefixes.length)]; | ||
| 61 | + const info = await supplierPage.generateRandomSupplierInfo({ | ||
| 62 | + namePrefix: namePrefix, | ||
| 63 | + }); | ||
| 64 | + | ||
| 65 | + console.log('新供应商名称:', info.name); | ||
| 66 | + console.log('新负责人:', info.managerName); | ||
| 67 | + console.log('新手机号:', info.phone); | ||
| 68 | + console.log('新备注:', info.remark); | ||
| 69 | + | ||
| 70 | + return info; | ||
| 71 | + }); | ||
| 72 | + | ||
| 73 | + // 步骤2:填写并提交供应商表单 | ||
| 74 | + await allure.step('填写并提交供应商表单', async () => { | ||
| 75 | + await supplierPage.navigateToSupplierManagement(); | ||
| 76 | + await supplierPage.clickSupplierItemByIndex(0); | ||
| 77 | + await supplierPage.clickEditButton(); | ||
| 78 | + await supplierPage.clearAndFillFormField(supplierPage.supplierNameInput, supplierInfo.name); | ||
| 79 | + await supplierPage.clearAndFillFormField(supplierPage.managerNameInput, supplierInfo.managerName); | ||
| 80 | + await supplierPage.clearAndFillFormField(supplierPage.phoneInput, supplierInfo.phone); | ||
| 81 | + await supplierPage.selectSupplierType(); | ||
| 82 | + await supplierPage.selectParkCard('888800010617'); | ||
| 83 | + await supplierPage.clearAndFillFormField(supplierPage.remarkInput, supplierInfo.remark || ''); | ||
| 84 | + await supplierPage.clickConfirmButton(); | ||
| 85 | + await supplierPage.attachScreenshot(testInfo, '编辑供应商成功截图'); | ||
| 86 | + }); | ||
| 87 | + | ||
| 88 | + // 步骤3:验证供应商修改成功 | ||
| 89 | + await allure.step('验证供应商修改成功', async () => { | ||
| 90 | + const isUpdated = await supplierPage.verifySupplierCreated(supplierInfo.name); | ||
| 91 | + expect(isUpdated).toBeTruthy(); | ||
| 92 | + }); | ||
| 93 | + }); | ||
| 94 | + | ||
| 95 | + test('删除供应商(金额都为0)', async ({ supplierPage }, testInfo) => { | ||
| 96 | + await allure.epic('供应商管理'); | ||
| 97 | + await allure.feature('供应商信息'); | ||
| 98 | + await allure.story('删除供应商'); | ||
| 99 | + | ||
| 100 | + // 步骤1:进入供应商管理页面 | ||
| 101 | + await allure.step('进入供应商管理页面', async () => { | ||
| 102 | + await supplierPage.navigateToSupplierManagement(); | ||
| 103 | + }); | ||
| 104 | + | ||
| 105 | + // 步骤2:查找并点击金额都为0的供应商 | ||
| 106 | + await allure.step('查找并点击金额都为0的供应商', async () => { | ||
| 107 | + const found = await supplierPage.clickSupplierWithZeroAmounts(); | ||
| 108 | + expect(found).toBeTruthy(); | ||
| 109 | + }); | ||
| 110 | + | ||
| 111 | + // 步骤3:点击删除按钮 | ||
| 112 | + await allure.step('点击删除按钮', async () => { | ||
| 113 | + await supplierPage.clickDeleteButton(); | ||
| 114 | + }); | ||
| 115 | + | ||
| 116 | + // 步骤4:确认删除 | ||
| 117 | + await allure.step('确认删除', async () => { | ||
| 118 | + await supplierPage.confirmDelete(); | ||
| 119 | + await supplierPage.attachScreenshot(testInfo, '删除供应商成功截图'); | ||
| 120 | + }); | ||
| 121 | + }); | ||
| 122 | + | ||
| 123 | + test('录入欠款并验证应付金额增加', async ({ supplierPage }, testInfo) => { | ||
| 124 | + await allure.epic('供应商管理'); | ||
| 125 | + await allure.feature('供应商信息'); | ||
| 126 | + await allure.story('录入欠款'); | ||
| 127 | + | ||
| 128 | + // 录入的欠款金额 | ||
| 129 | + const debtAmount = '100'; | ||
| 130 | + const remark = '测试录入欠款'; | ||
| 131 | + | ||
| 132 | + // 步骤1:进入供应商管理页面并选择第2个供应商,记录原始应付金额和供应商名称 | ||
| 133 | + let supplierName = ''; | ||
| 134 | + const originalAmount = await allure.step('记录原始应付金额', async () => { | ||
| 135 | + await supplierPage.navigateToSupplierManagement(); | ||
| 136 | + await supplierPage.clickSupplierItemByIndex(1); | ||
| 137 | + await supplierPage.waitForPageLoad(); | ||
| 138 | + await supplierPage.wait(300); | ||
| 139 | + supplierName = await supplierPage.getSupplierName(); | ||
| 140 | + return await supplierPage.parsePayableAmount(); | ||
| 141 | + }); | ||
| 142 | + console.log('录入前应付金额:', originalAmount); | ||
| 143 | + | ||
| 144 | + // 步骤2:点击录入欠款 | ||
| 145 | + await allure.step('点击录入欠款', async () => { | ||
| 146 | + await supplierPage.clickRecordDebt(); | ||
| 147 | + }); | ||
| 148 | + | ||
| 149 | + // 步骤3:选择欠款方向(我方欠供应商) | ||
| 150 | + await allure.step('选择欠款方向', async () => { | ||
| 151 | + await supplierPage.selectDebtDirection('oweSupplier'); | ||
| 152 | + }); | ||
| 153 | + | ||
| 154 | + // 步骤4:选择赊欠日期(今日) | ||
| 155 | + await allure.step('选择赊欠日期', async () => { | ||
| 156 | + await supplierPage.selectDebtDate(); | ||
| 157 | + }); | ||
| 158 | + | ||
| 159 | + // 步骤5:填写赊欠金额 | ||
| 160 | + await allure.step('填写赊欠金额', async () => { | ||
| 161 | + await supplierPage.fillDebtAmount(debtAmount); | ||
| 162 | + }); | ||
| 163 | + | ||
| 164 | + // 步骤6:填写备注 | ||
| 165 | + await allure.step('填写备注', async () => { | ||
| 166 | + await supplierPage.fillRemark(remark); | ||
| 167 | + }); | ||
| 168 | + | ||
| 169 | + // 步骤7:保存欠款记录 | ||
| 170 | + await allure.step('保存欠款记录', async () => { | ||
| 171 | + await supplierPage.saveDebt(); | ||
| 172 | + }); | ||
| 173 | + | ||
| 174 | + // 步骤8:验证欠款录入成功 - 搜索同一供应商查看应付金额 | ||
| 175 | + const currentAmount = await allure.step('验证欠款录入成功', async () => { | ||
| 176 | + await supplierPage.navigateToSupplierManagement(); | ||
| 177 | + await supplierPage.clickSupplierItemByIndex(1); | ||
| 178 | + await supplierPage.page.waitForLoadState('networkidle'); | ||
| 179 | + await supplierPage.page.waitForTimeout(300); | ||
| 180 | + const amount = await supplierPage.parsePayableAmount(); | ||
| 181 | + console.log('录入后应付金额:', amount); | ||
| 182 | + return amount; | ||
| 183 | + }); | ||
| 184 | + | ||
| 185 | + // 验证应付金额增加了录入的金额 | ||
| 186 | + expect(currentAmount).toBeCloseTo(originalAmount + parseFloat(debtAmount), 1); | ||
| 187 | + | ||
| 188 | + await supplierPage.attachScreenshot(testInfo, '录入欠款成功截图'); | ||
| 189 | + }); | ||
| 190 | +}); |
tests/supplier_grouping.spec.ts
0 → 100644
| 1 | +import { test, expect } from '../fixtures'; | ||
| 2 | +import * as allure from 'allure-js-commons'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 供应商分组管理测试 | ||
| 6 | + */ | ||
| 7 | +test.describe('供应商分组管理', () => { | ||
| 8 | + // 使用已保存的认证状态 | ||
| 9 | + test.use({ storageState: 'auth.json' }); | ||
| 10 | + | ||
| 11 | + // 强制测试串行执行,避免并行测试之间的干扰 | ||
| 12 | + test.describe.configure({ mode: 'serial' }); | ||
| 13 | + | ||
| 14 | + test('新增供应商分组', async ({ supplierGroupingPage }, testInfo) => { | ||
| 15 | + await allure.epic('供应商管理'); | ||
| 16 | + await allure.feature('供应商分组'); | ||
| 17 | + await allure.story('新增供应商分组'); | ||
| 18 | + | ||
| 19 | + // 步骤1:生成随机分组信息 | ||
| 20 | + const groupName = `自动${Date.now().toString().slice(-3)}`; | ||
| 21 | + const remark = '自动化测试'; | ||
| 22 | + | ||
| 23 | + // 步骤2:进入供应商分组页面并点击新建 | ||
| 24 | + await allure.step('进入供应商分组页面', async () => { | ||
| 25 | + await supplierGroupingPage.navigateToSupplierGrouping(); | ||
| 26 | + await supplierGroupingPage.clickAddButton(); | ||
| 27 | + }); | ||
| 28 | + | ||
| 29 | + // 步骤3:填写分组信息 | ||
| 30 | + await allure.step('填写分组信息', async () => { | ||
| 31 | + await supplierGroupingPage.fillGroupName(groupName); | ||
| 32 | + await supplierGroupingPage.fillRemark(remark); | ||
| 33 | + }); | ||
| 34 | + | ||
| 35 | + // 步骤4:保存分组 | ||
| 36 | + await allure.step('保存分组', async () => { | ||
| 37 | + await supplierGroupingPage.saveGrouping(); | ||
| 38 | + await supplierGroupingPage.attachScreenshot(testInfo, '新增供应商分组成功截图'); | ||
| 39 | + }); | ||
| 40 | + | ||
| 41 | + // 步骤5:验证分组创建成功 | ||
| 42 | + await allure.step('验证分组创建成功', async () => { | ||
| 43 | + const isCreated = await supplierGroupingPage.verifyGroupingCreated(groupName); | ||
| 44 | + expect(isCreated).toBeTruthy(); | ||
| 45 | + }); | ||
| 46 | + }); | ||
| 47 | + | ||
| 48 | + test('修改供应商分组', async ({ supplierGroupingPage }, testInfo) => { | ||
| 49 | + await allure.epic('供应商管理'); | ||
| 50 | + await allure.feature('供应商分组'); | ||
| 51 | + await allure.story('修改供应商分组'); | ||
| 52 | + | ||
| 53 | + // 步骤1:生成新的分组信息 | ||
| 54 | + const newGroupName = `修改${Date.now().toString().slice(-2)}`; | ||
| 55 | + const newRemark = '自动化测试修改'; | ||
| 56 | + | ||
| 57 | + // 步骤2:进入供应商分组页面 | ||
| 58 | + await allure.step('进入供应商分组页面', async () => { | ||
| 59 | + await supplierGroupingPage.navigateToSupplierGrouping(); | ||
| 60 | + }); | ||
| 61 | + | ||
| 62 | + // 步骤3:点击编辑分组 | ||
| 63 | + await allure.step('点击编辑分组', async () => { | ||
| 64 | + await supplierGroupingPage.clickEditButton(); | ||
| 65 | + }); | ||
| 66 | + | ||
| 67 | + // 步骤4:修改分组信息 | ||
| 68 | + await allure.step('修改分组信息', async () => { | ||
| 69 | + await supplierGroupingPage.updateGrouping(newGroupName, newRemark); | ||
| 70 | + await supplierGroupingPage.attachScreenshot(testInfo, '修改供应商分组成功截图'); | ||
| 71 | + }); | ||
| 72 | + | ||
| 73 | + // 步骤5:验证分组修改成功 | ||
| 74 | + await allure.step('验证分组修改成功', async () => { | ||
| 75 | + const isUpdated = await supplierGroupingPage.verifyGroupingUpdated(newGroupName); | ||
| 76 | + expect(isUpdated).toBeTruthy(); | ||
| 77 | + }); | ||
| 78 | + }); | ||
| 79 | + | ||
| 80 | + test('删除供应商分组', async ({ supplierGroupingPage }, testInfo) => { | ||
| 81 | + await allure.epic('供应商管理'); | ||
| 82 | + await allure.feature('供应商分组'); | ||
| 83 | + await allure.story('删除供应商分组'); | ||
| 84 | + | ||
| 85 | + // 步骤1:生成待删除的分组名称 | ||
| 86 | + const groupName = `删除${Date.now().toString().slice(-2)}`; | ||
| 87 | + | ||
| 88 | + // 步骤2:先创建一个分组用于删除 | ||
| 89 | + await allure.step('创建待删除的分组', async () => { | ||
| 90 | + await supplierGroupingPage.navigateToSupplierGrouping(); | ||
| 91 | + await supplierGroupingPage.clickAddButton(); | ||
| 92 | + await supplierGroupingPage.fillGroupName(groupName); | ||
| 93 | + await supplierGroupingPage.fillRemark('自动化测试'); | ||
| 94 | + await supplierGroupingPage.saveGrouping(); | ||
| 95 | + }); | ||
| 96 | + | ||
| 97 | + // 步骤3:删除分组 | ||
| 98 | + await allure.step('删除分组', async () => { | ||
| 99 | + await supplierGroupingPage.navigateToSupplierGrouping(); | ||
| 100 | + await supplierGroupingPage.clickGroupingItem(groupName); | ||
| 101 | + await supplierGroupingPage.clickDeleteButton(); | ||
| 102 | + await supplierGroupingPage.confirmDelete(); | ||
| 103 | + await supplierGroupingPage.attachScreenshot(testInfo, '删除供应商分组成功截图'); | ||
| 104 | + }); | ||
| 105 | + | ||
| 106 | + // 步骤4:验证分组删除成功 | ||
| 107 | + await allure.step('验证分组删除成功', async () => { | ||
| 108 | + const isDeleted = await supplierGroupingPage.verifyGroupingDeleted(groupName); | ||
| 109 | + expect(isDeleted).toBeTruthy(); | ||
| 110 | + }); | ||
| 111 | + }); | ||
| 112 | +}); |
utils/dataGenerator.ts
| @@ -34,6 +34,31 @@ export function generateCustomerName(): string { | @@ -34,6 +34,31 @@ export function generateCustomerName(): string { | ||
| 34 | return `${familyName}${givenName}${timestamp}`; | 34 | return `${familyName}${givenName}${timestamp}`; |
| 35 | } | 35 | } |
| 36 | 36 | ||
| 37 | +// 随机生成中文姓名(用于负责人等场景) | ||
| 38 | +export function generateChineseName(): string { | ||
| 39 | + const firstNames = ['张', '李', '王', '刘', '陈', '杨', '黄', '赵', '周', '吴', '徐', '孙', '马', '朱', '胡', '郭', '何', '林', '高', '罗']; | ||
| 40 | + const lastNames = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '涛', '明', '超', '秀英', '建国', '志强', '晓明']; | ||
| 41 | + const firstName = firstNames[Math.floor(Math.random() * firstNames.length)]; | ||
| 42 | + const lastName = lastNames[Math.floor(Math.random() * lastNames.length)]; | ||
| 43 | + return `${firstName}${lastName}`; | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +// 随机生成供应商名称 | ||
| 47 | +export function generateSupplierName(): string { | ||
| 48 | + const prefixes = ['华', '中', '东', '西', '南', '北', '安', '盛', '祥', '隆', '鑫', '福', '源', '顺', '昌', '聚', '恒', '威', '腾', '宇']; | ||
| 49 | + const suffixes = ['源', '丰', '盛', '达', '祥', '隆', '福', '禄', '宝', '瑞', '昌', '华', '兴', '光', '天', '地', '人和', '利', '盈', '发']; | ||
| 50 | + const businessTypes = ['商贸', '实业', '贸易', '供应', '物流', '配送', '批发', '科技', '材料', '能源']; | ||
| 51 | + | ||
| 52 | + const prefix = prefixes[Math.floor(Math.random() * prefixes.length)]; | ||
| 53 | + const suffix = suffixes[Math.floor(Math.random() * suffixes.length)]; | ||
| 54 | + const businessType = businessTypes[Math.floor(Math.random() * businessTypes.length)]; | ||
| 55 | + | ||
| 56 | + // 使用时间戳(后6位)确保不易重复 | ||
| 57 | + const timestamp = Date.now().toString().slice(-6); | ||
| 58 | + | ||
| 59 | + return `自动化${prefix}${suffix}${businessType}${timestamp}`; | ||
| 60 | +} | ||
| 61 | + | ||
| 37 | // 随机生成身份证号码(18位) | 62 | // 随机生成身份证号码(18位) |
| 38 | export function generateIdCard(): string { | 63 | export function generateIdCard(): string { |
| 39 | // 省份代码 | 64 | // 省份代码 |