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 | 4 | import { SalePage } from '../pages/salePage'; |
| 5 | 5 | import { ConsignmentPage } from '../pages/consignmentPage'; |
| 6 | 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 | 35 | * 客户管理页面 |
| 34 | 36 | */ |
| 35 | 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 | 75 | customerPage: async ({ page }, use) => { |
| 64 | 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 | 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 | 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 | 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 | 62 | // 随机生成身份证号码(18位) |
| 38 | 63 | export function generateIdCard(): string { |
| 39 | 64 | // 省份代码 | ... | ... |