Commit bbe2eb880cf6eaca297199b24e31641bcd8ffb2d

Authored by 李玉霞
1 parent 486d0cf6

新增供应商管理用例

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,7 @@ 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';
7 8  
8 9 /**
9 10 * 页面对象夹具类型定义
... ... @@ -33,6 +34,11 @@ export type PageFixtures = {
33 34 * 客户管理页面
34 35 */
35 36 customerPage: CustomerPage;
  37 +
  38 + /**
  39 + * 供应商管理页面
  40 + */
  41 + supplierPage: SupplierPage;
36 42 };
37 43  
38 44 /**
... ... @@ -63,6 +69,11 @@ export const test = base.extend<PageFixtures>({
63 69 customerPage: async ({ page }, use) => {
64 70 await use(new CustomerPage(page));
65 71 },
  72 +
  73 + // 供应商管理页面
  74 + supplierPage: async ({ page }, use) => {
  75 + await use(new SupplierPage(page));
  76 + },
66 77 });
67 78  
68 79 /**
... ... @@ -100,4 +111,8 @@ export const fullTest = authTest.extend<PageFixtures>({
100 111 await use(new CustomerPage(authenticatedPage));
101 112 },
102 113  
  114 + supplierPage: async ({authenticatedPage }, use) => {
  115 + await use(new SupplierPage(authenticatedPage));
  116 + },
  117 +
103 118 });
104 119 \ No newline at end of file
... ...
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(1).click();
  376 + await this.page.getByText('0', { exact: true }).nth(1).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' || trimmed === '¥0';
  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_add.spec.ts 0 → 100644
  1 +import { test, expect } from '../fixtures';
  2 +import * as allure from 'allure-js-commons';
  3 +
  4 +/**
  5 + * 供应商管理测试
  6 + */
  7 +// 新增供应商
  8 +test.describe('供应商管理', () => {
  9 + // 使用已保存的认证状态
  10 + test.use({ storageState: 'auth.json' });
  11 +
  12 + // 强制测试串行执行,避免并行测试之间的干扰
  13 + test.describe.configure({ mode: 'serial' });
  14 +
  15 + test('新建供应商并绑定园区卡', async ({ supplierPage }, testInfo) => {
  16 + // 添加allure元素
  17 + await allure.epic('供应商管理');
  18 + await allure.feature('供应商信息');
  19 + await allure.story('新建供应商');
  20 +
  21 + // 步骤1:生成随机供应商信息
  22 + const supplierInfo = await allure.step('生成随机供应商信息', async () => {
  23 + const info = await supplierPage.generateRandomSupplierInfo();
  24 +
  25 + console.log('供应商名称:', info.name);
  26 + console.log('负责人:', info.managerName);
  27 + console.log('手机号:', info.phone);
  28 + console.log('备注:', info.remark);
  29 +
  30 + return info;
  31 + });
  32 +
  33 + // 步骤2:填写并提交供应商表单
  34 + await allure.step('填写并提交供应商表单', async () => {
  35 + // 进入供应商管理页面
  36 + await supplierPage.navigateToSupplierManagement();
  37 +
  38 + // 点击新建供应商
  39 + await supplierPage.clickAddSupplier();
  40 +
  41 + // 填写供应商名称
  42 + await supplierPage.fillFormField(supplierPage.supplierNameInput, supplierInfo.name);
  43 +
  44 + // 选择供应商分组
  45 + await supplierPage.selectGroup();
  46 +
  47 + // 填写负责人名称
  48 + await supplierPage.fillFormField(supplierPage.managerNameInput, supplierInfo.managerName);
  49 +
  50 + // 填写手机号
  51 + await supplierPage.fillFormField(supplierPage.phoneInput, supplierInfo.phone);
  52 +
  53 + // 绑定园区卡
  54 + await supplierPage.selectParkCard('888810046477', '地利');
  55 +
  56 + // 填写备注
  57 + await supplierPage.fillFormField(supplierPage.remarkInput, supplierInfo.remark || '');
  58 +
  59 + // 保存
  60 + await supplierPage.saveButton.click();
  61 +
  62 + await supplierPage.attachScreenshot(testInfo, '新建供应商成功截图');
  63 + });
  64 +
  65 + // 步骤3:验证供应商创建成功
  66 + await allure.step('验证供应商创建成功', async () => {
  67 + const isCreated = await supplierPage.verifySupplierCreated(supplierInfo.name);
  68 + expect(isCreated).toBeTruthy();
  69 + });
  70 + });
  71 +});
... ...
tests/supplier_bill.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 + // 添加allure元素
  16 + await allure.epic('供应商管理');
  17 + await allure.feature('供应商信息');
  18 + await allure.story('录入欠款');
  19 +
  20 + // 录入的欠款金额
  21 + const debtAmount = '100';
  22 + const remark = '测试录入欠款';
  23 +
  24 + // 步骤1:进入供应商管理页面并选择第2个供应商,记录原始应付金额和供应商名称
  25 + let supplierName = '';
  26 + const originalAmount = await allure.step('记录原始应付金额', async () => {
  27 + await supplierPage.navigateToSupplierManagement();
  28 + await supplierPage.clickSupplierItemByIndex(1);
  29 + await supplierPage.page.waitForLoadState('networkidle');
  30 + await supplierPage.page.waitForTimeout(300);
  31 + return await supplierPage.parsePayableAmount();
  32 + });
  33 + console.log('录入前应付金额:', originalAmount);
  34 +
  35 + // 步骤2:点击录入欠款
  36 + await allure.step('点击录入欠款', async () => {
  37 + await supplierPage.clickRecordDebt();
  38 + });
  39 +
  40 + // 步骤3:选择欠款方向(我方欠供应商)
  41 + await allure.step('选择欠款方向', async () => {
  42 + await supplierPage.selectDebtDirection('oweSupplier');
  43 + });
  44 +
  45 + // 步骤4:选择赊欠日期(今日)
  46 + await allure.step('选择赊欠日期', async () => {
  47 + await supplierPage.selectDebtDate();
  48 + });
  49 +
  50 + // 步骤5:填写赊欠金额
  51 + await allure.step('填写赊欠金额', async () => {
  52 + await supplierPage.fillDebtAmount(debtAmount);
  53 + });
  54 +
  55 + // 步骤6:填写备注
  56 + await allure.step('填写备注', async () => {
  57 + await supplierPage.fillRemark(remark);
  58 + });
  59 +
  60 + // 步骤7:保存欠款记录
  61 + await allure.step('保存欠款记录', async () => {
  62 + await supplierPage.saveDebt();
  63 + });
  64 +
  65 + // 步骤8:验证欠款录入成功 - 搜索同一供应商查看应付金额
  66 + const currentAmount = await allure.step('验证欠款录入成功', async () => {
  67 + await supplierPage.navigateToSupplierManagement();
  68 + await supplierPage.clickSupplierItemByIndex(1);
  69 + await supplierPage.page.waitForLoadState('networkidle');
  70 + await supplierPage.page.waitForTimeout(300);
  71 + const amount = await supplierPage.parsePayableAmount();
  72 + console.log('录入后应付金额:', amount);
  73 + return amount;
  74 + });
  75 +
  76 + // 验证应付金额增加了录入的金额
  77 + expect(currentAmount).toBeCloseTo(originalAmount + parseFloat(debtAmount), 1);
  78 +
  79 + await supplierPage.attachScreenshot(testInfo, '录入欠款成功截图');
  80 + });
  81 +});
0 82 \ No newline at end of file
... ...
tests/supplier_delete.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('删除供应商(金额都为0)', async ({ supplierPage }, testInfo) => {
  15 + // 添加allure元素
  16 + await allure.epic('供应商管理');
  17 + await allure.feature('供应商信息');
  18 + await allure.story('删除供应商');
  19 +
  20 + // 步骤1:进入供应商管理页面
  21 + await allure.step('进入供应商管理页面', async () => {
  22 + await supplierPage.navigateToSupplierManagement();
  23 + });
  24 +
  25 + // 步骤2:查找并点击金额都为0的供应商
  26 + await allure.step('查找并点击金额都为0的供应商', async () => {
  27 + const found = await supplierPage.clickSupplierWithZeroAmounts();
  28 + expect(found).toBeTruthy();
  29 + });
  30 +
  31 + // 步骤3:点击删除按钮
  32 + await allure.step('点击删除按钮', async () => {
  33 + await supplierPage.clickDeleteButton();
  34 + });
  35 +
  36 + // 步骤4:确认删除
  37 + await allure.step('确认删除', async () => {
  38 + await supplierPage.confirmDelete();
  39 + await supplierPage.attachScreenshot(testInfo, '删除供应商成功截图');
  40 + });
  41 + });
  42 +});
0 43 \ No newline at end of file
... ...
tests/supplier_update.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 + // 添加allure元素
  16 + await allure.epic('供应商管理');
  17 + await allure.feature('供应商信息');
  18 + await allure.story('编辑供应商');
  19 +
  20 + // 步骤1:生成新的供应商信息
  21 + const supplierInfo = await allure.step('生成新的供应商信息', async () => {
  22 + // 随机生成供应商名称前缀
  23 + const namePrefixes = ['修改', '更新', '调整', '变更'];
  24 + const namePrefix = namePrefixes[Math.floor(Math.random() * namePrefixes.length)];
  25 +
  26 + const info = await supplierPage.generateRandomSupplierInfo({
  27 + namePrefix: namePrefix,
  28 + });
  29 +
  30 + console.log('新供应商名称:', info.name);
  31 + console.log('新负责人:', info.managerName);
  32 + console.log('新手机号:', info.phone);
  33 + console.log('新备注:', info.remark);
  34 +
  35 + return info;
  36 + });
  37 +
  38 + // 步骤2:填写并提交供应商表单
  39 + await allure.step('填写并提交供应商表单', async () => {
  40 + // 进入供应商管理页面
  41 + await supplierPage.navigateToSupplierManagement();
  42 +
  43 + // 选择供应商列表项(点击第一个)
  44 + await supplierPage.clickSupplierItemByIndex(0);
  45 +
  46 + // 点击编辑按钮
  47 + await supplierPage.clickEditButton();
  48 +
  49 + // 修改供应商名称
  50 + await supplierPage.clearAndFillFormField(supplierPage.supplierNameInput, supplierInfo.name);
  51 +
  52 + // 修改负责人名称
  53 + await supplierPage.clearAndFillFormField(supplierPage.managerNameInput, supplierInfo.managerName);
  54 +
  55 + // 修改手机号
  56 + await supplierPage.clearAndFillFormField(supplierPage.phoneInput, supplierInfo.phone);
  57 +
  58 + // 选择供应商类型(必选字段)
  59 + await supplierPage.selectSupplierType();
  60 +
  61 + // 绑定园区卡
  62 + await supplierPage.selectParkCard('888800010617');
  63 +
  64 + // 修改备注
  65 + await supplierPage.clearAndFillFormField(supplierPage.remarkInput, supplierInfo.remark || '');
  66 +
  67 + // 保存
  68 + await supplierPage.clickConfirmButton();
  69 +
  70 + await supplierPage.attachScreenshot(testInfo, '编辑供应商成功截图');
  71 + });
  72 +
  73 + // 步骤3:验证供应商修改成功
  74 + await allure.step('验证供应商修改成功', async () => {
  75 + const isUpdated = await supplierPage.verifySupplierCreated(supplierInfo.name);
  76 + expect(isUpdated).toBeTruthy();
  77 + });
  78 + });
  79 +});
0 80 \ No newline at end of file
... ...
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 // 省份代码
... ...