Commit 140ac106a403710398d8315d427bace0e7c2a92d

Authored by 赵旭婷
1 parent 33d656b8

新增客户管理

fixtures/testFixture.ts
... ... @@ -3,6 +3,7 @@ import { LoginPage } from '../pages/loginPage';
3 3 import { ProductPage } from '../pages/productPage';
4 4 import { SalePage } from '../pages/salePage';
5 5 import { ConsignmentPage } from '../pages/consignmentPage';
  6 +import { CustomerPage } from '../pages/customerPage';
6 7  
7 8 /**
8 9 * 页面对象夹具类型定义
... ... @@ -27,6 +28,11 @@ export type PageFixtures = {
27 28 * 代销入库页面
28 29 */
29 30 consignmentPage: ConsignmentPage;
  31 +
  32 + /**
  33 + * 客户管理页面
  34 + */
  35 + customerPage: CustomerPage;
30 36 };
31 37  
32 38 /**
... ... @@ -52,6 +58,11 @@ export const test = base.extend<PageFixtures>({
52 58 consignmentPage: async ({ page }, use) => {
53 59 await use(new ConsignmentPage(page));
54 60 },
  61 +
  62 + // 客户管理页面
  63 + customerPage: async ({ page }, use) => {
  64 + await use(new CustomerPage(page));
  65 + },
55 66 });
56 67  
57 68 /**
... ... @@ -85,4 +96,8 @@ export const fullTest = authTest.extend<PageFixtures>({
85 96 consignmentPage: async ({ authenticatedPage }, use) => {
86 97 await use(new ConsignmentPage(authenticatedPage));
87 98 },
88   -});
89 99 \ No newline at end of file
  100 + customerPage: async ({authenticatedPage }, use) =>{
  101 + await use(new CustomerPage(authenticatedPage));
  102 + },
  103 +
  104 + });
90 105 \ No newline at end of file
... ...
pages/consignmentPage.ts
... ... @@ -169,6 +169,37 @@ export class ConsignmentPage extends BasePage {
169 169 await this.productListButton.click();
170 170 }
171 171  
  172 + // /**
  173 + // * 获取商品列表中所有可用的商品名称
  174 + // * @returns 商品名称数组
  175 + // */
  176 + // async getAvailableProducts(): Promise<string[]> {
  177 + // // 等待商品列表加载
  178 + // await this.page.locator('.productName').first().waitFor({ state: 'visible', timeout: 5000 });
  179 +
  180 + // // 获取所有商品名称
  181 + // const productElements = await this.page.locator('.productName').allTextContents();
  182 + // return productElements.filter(name => name.trim() !== '');
  183 + // }
  184 +
  185 + // /**
  186 + // * 随机选择一个商品
  187 + // * @returns 随机选择的商品名称
  188 + // */
  189 + // async selectRandomProduct(): Promise<string> {
  190 + // const products = await this.getAvailableProducts();
  191 +
  192 + // if (products.length === 0) {
  193 + // throw new Error('商品列表为空,无法选择商品');
  194 + // }
  195 +
  196 + // const randomIndex = Math.floor(Math.random() * products.length);
  197 + // const selectedProduct = products[randomIndex];
  198 + // console.log(`随机选择的商品: ${selectedProduct}`);
  199 +
  200 + // return selectedProduct;
  201 + // }
  202 +
172 203 /**
173 204 * 选择商品
174 205 * @param productName 商品名称
... ...
pages/customerPage.ts 0 → 100644
  1 +import { Page, Locator, expect } from '@playwright/test';
  2 +import { BasePage } from './basePage';
  3 +
  4 +/**
  5 + * 客户管理页面选择器
  6 + */
  7 +const selectors = {
  8 + // 导航
  9 + customerMenu: 'text=客户管理',
  10 + addCustomerButton: 'text=新增客户',
  11 +
  12 + // 表单字段
  13 + customerNameInput: 'textbox', // 客户名称输入框
  14 + phoneInput: 'spinbutton', // 手机号输入框
  15 + idCardInput: 'textbox', // 身份证输入框
  16 +
  17 + // 客户分组
  18 + customerGroupPicker: '.nut-input__mask',
  19 + normalCustomerOption: 'uni-view:has-text("普通客户")',
  20 +
  21 + // 赊欠额度
  22 + creditLimitSwitch: '.nut-form-item__body__slots > .nut-switch',
  23 + creditLimitInput: 'uni-input:has-text("请输入赊欠额度/不输为不限额") input',
  24 +
  25 + // 车牌号
  26 + // licensePlateInput: '.nut-input__mask',
  27 + licensePlateInput:'uni-view:nth-child(11) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask',
  28 + confirmLicenceButton:'text=确定',
  29 + // 省市区选择
  30 + regionPicker: 'uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask',
  31 + confirmRegionButton: 'text=确认选择',
  32 +
  33 + // 详细地址
  34 + detailedAddressInput: 'uni-view:nth-child(13) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input',
  35 +
  36 + // 操作按钮
  37 + confirmButton: 'text=确定',
  38 + saveButton: 'text=保存',
  39 +
  40 + // 验证
  41 + customerInList: (customerName: string) => `uni-view:has-text("${customerName}")`,
  42 +};
  43 +
  44 +/**
  45 + * 客户管理页面类
  46 + * 处理客户管理相关操作
  47 + */
  48 +export class CustomerPage extends BasePage {
  49 + // 导航定位器
  50 + readonly customerMenu: Locator;
  51 + readonly addCustomerButton: Locator;
  52 +
  53 + // 表单字段
  54 + readonly customerNameInput: Locator;
  55 + readonly phoneInput: Locator;
  56 + readonly idCardInput: Locator;
  57 +
  58 + // 客户分组
  59 + readonly customerGroupPicker: Locator;
  60 + readonly normalCustomerOption: Locator;
  61 +
  62 + // 赊欠额度
  63 + readonly creditLimitSwitch: Locator;
  64 + readonly creditLimitInput: Locator;
  65 +
  66 + // 车牌号
  67 + readonly licensePlateInput: Locator;
  68 +
  69 + // 省市区选择
  70 + readonly regionPicker: Locator;
  71 + readonly confirmRegionButton: Locator;
  72 +
  73 + // 详细地址
  74 + readonly detailedAddressInput: Locator;
  75 +
  76 + // 操作按钮
  77 + readonly confirmButton: Locator;
  78 + readonly saveButton: Locator;
  79 +
  80 + constructor(page: Page) {
  81 + super(page);
  82 +
  83 + this.customerMenu = page.getByText('客户管理');
  84 + this.addCustomerButton = page.getByText('新增客户');
  85 + this.customerNameInput = page.getByRole('textbox').nth(1);
  86 + this.phoneInput = page.getByRole('spinbutton').first();
  87 + this.idCardInput = page.getByRole('textbox').nth(2);
  88 + this.customerGroupPicker = page.locator('.nut-input__mask').first();
  89 + this.normalCustomerOption = page.locator('uni-view').filter({ hasText: /^普通客户$/ }).nth(3);
  90 + this.creditLimitSwitch = page.locator('.nut-form-item__body__slots > .nut-switch');
  91 + this.creditLimitInput = page.locator('uni-input').filter({ hasText: '请输入赊欠额度/不输为不限额' }).getByRole('spinbutton');
  92 + this.licensePlateInput = page.locator('.nut-input__mask').first();
  93 + this.regionPicker = page.locator('uni-view:nth-child(11) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask');
  94 + this.confirmRegionButton = page.getByText('确认选择');
  95 + this.detailedAddressInput = page.locator('uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input');
  96 + this.confirmButton = page.getByText('确定');
  97 + this.saveButton = page.getByText('保存');
  98 + }
  99 +
  100 + /**
  101 + * 导航到首页并等待加载
  102 + */
  103 + async gotoHome(): Promise<void> {
  104 + await this.navigate('/');
  105 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  106 + }
  107 +
  108 + /**
  109 + * 打开客户管理页面
  110 + */
  111 + async openCustomerManagement(): Promise<void> {
  112 + await this.customerMenu.click();
  113 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  114 + }
  115 +
  116 + /**
  117 + * 点击新增客户按钮
  118 + */
  119 + async clickAddCustomer(): Promise<void> {
  120 + await this.addCustomerButton.click();
  121 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  122 + }
  123 +
  124 + /**
  125 + * 填写客户名称
  126 + * @param name 客户名称
  127 + */
  128 + async fillCustomerName(name: string): Promise<void> {
  129 + await this.customerNameInput.click();
  130 + await this.customerNameInput.fill(name);
  131 + }
  132 +
  133 + /**
  134 + * 填写手机号
  135 + * @param phone 手机号
  136 + */
  137 + async fillPhone(phone: string): Promise<void> {
  138 + await this.phoneInput.click();
  139 + await this.phoneInput.fill(phone);
  140 + }
  141 +
  142 + /**
  143 + * 填写身份证号
  144 + * @param idCard 身份证号
  145 + */
  146 + async fillIdCard(idCard: string): Promise<void> {
  147 + await this.idCardInput.click();
  148 + await this.idCardInput.fill(idCard);
  149 + }
  150 +
  151 + /**
  152 + * 选择客户分组(选择"普通客户")
  153 + */
  154 + async selectCustomerGroup(): Promise<void> {
  155 + // 点击客户分组选择器
  156 + await this.customerGroupPicker.click();
  157 +
  158 + // 等待弹窗出现
  159 + await this.page.waitForTimeout(500);
  160 +
  161 + // 选择"普通客户"(使用 nth(4) 和 force: true 绕过遮罩层)
  162 + await this.page.locator('uni-view').filter({ hasText: /^普通客户$/ }).nth(4).click({ force: true });
  163 + }
  164 +
  165 + /**
  166 + * 开启赊欠额度开关
  167 + */
  168 + async enableCreditLimit(): Promise<void> {
  169 + await this.creditLimitSwitch.click();
  170 + }
  171 +
  172 + /**
  173 + * 填写赊欠额度
  174 + * @param amount 赊欠额度金额
  175 + */
  176 + async fillCreditLimit(amount: string): Promise<void> {
  177 + await this.creditLimitInput.click();
  178 + await this.creditLimitInput.fill(amount);
  179 + }
  180 +
  181 + /**
  182 + * 填写车牌号
  183 + * @param licensePlate 车牌号(如:渝ZY0706)
  184 + */
  185 + async fillLicensePlate(licensePlate: string): Promise<void> {
  186 + // 等待页面稳定
  187 + await this.page.waitForTimeout(500);
  188 +
  189 + // 点击车牌号输入框 - nth-child(11),开启赊欠额度后位置
  190 + await this.page.locator('uni-view:nth-child(11) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask').click();
  191 +
  192 + // 等待车牌键盘面板加载完成
  193 + await this.page.waitForTimeout(1000);
  194 +
  195 + // 解析车牌号的省份和字母部分
  196 + const province = licensePlate.charAt(0); // 如:渝
  197 + const letter = licensePlate.charAt(1); // 如:Z
  198 + const numbers = licensePlate.substring(2); // 如:Y0706
  199 +
  200 + // 点击省份
  201 + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${province}$`) }).click();
  202 +
  203 + // 等待字母键盘出现
  204 + await this.page.waitForTimeout(300);
  205 +
  206 + // 点击第一个字母(需要用 nth(1) 选择第二个匹配项)
  207 + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${letter}$`) }).nth(1).click();
  208 +
  209 + // 逐个点击车牌号码
  210 + for (let i = 0; i < numbers.length; i++) {
  211 + const char = numbers[i];
  212 + await this.page.waitForTimeout(100);
  213 + if (char.match(/[A-Za-z]/)) {
  214 + // 字母
  215 + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char.toUpperCase()}$`) }).click();
  216 + } else if (char.match(/[0-9]/)) {
  217 + // 数字 - 检查是否需要使用 nth(1)
  218 + const digitCount = await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) }).count();
  219 + if (digitCount > 1) {
  220 + // 如果有多个相同数字,点击第二个
  221 + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) }).nth(1).click();
  222 + } else {
  223 + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) }).click();
  224 + }
  225 + }
  226 + }
  227 +
  228 + // 点击确定按钮
  229 + await this.page.getByText('确定').click();
  230 + }
  231 +
  232 + /**
  233 + * 选择省市区
  234 + * @param province 省份(如:山西省)
  235 + * @param city 城市(如:长治市)
  236 + * @param district 区县(如:潞城区)
  237 + */
  238 + async selectRegion(province: string, city: string, district: string): Promise<void> {
  239 + // 点击省市区选择器 - nth-child(12),开启赊欠额度后位置
  240 + await this.page.locator('uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask').click();
  241 +
  242 + // 等待弹窗出现
  243 + await this.page.waitForTimeout(500);
  244 +
  245 + // 选择省份
  246 + await this.page.getByText(province).click();
  247 +
  248 + // 选择城市
  249 + await this.page.getByText(city).click();
  250 +
  251 + // 选择区县
  252 + await this.page.getByText(district).click();
  253 +
  254 + // 确认选择
  255 + await this.page.getByText('确认选择').click();
  256 + }
  257 +
  258 + /**
  259 + * 填写详细地址
  260 + * @param address 详细地址
  261 + */
  262 + async fillDetailedAddress(address: string): Promise<void> {
  263 + // 点击详细地址输入框 - nth-child(13),开启赊欠额度后位置
  264 + await this.page.locator('uni-view:nth-child(13) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input').click();
  265 + // 填写详细地址
  266 + await this.page.locator('uni-view:nth-child(13) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input').fill(address);
  267 + }
  268 +
  269 + /**
  270 + * 上传客户图片
  271 + * @param imagePath 图片文件路径(相对于项目根目录)
  272 + */
  273 + async uploadCustomerImage(imagePath: string): Promise<void> {
  274 + // 使用 Playwright 的 file_chooser 事件处理文件上传
  275 + // 监听文件选择器事件
  276 + const fileChooserPromise = this.page.waitForEvent('filechooser');
  277 +
  278 + // 点击上传按钮触发文件选择器
  279 + await this.page.locator('uni-scroll-view uni-button').click();
  280 +
  281 + // 等待文件选择器出现并设置文件
  282 + const fileChooser = await fileChooserPromise;
  283 + await fileChooser.setFiles(imagePath);
  284 +
  285 + // 等待上传完成
  286 + await this.page.waitForTimeout(1000);
  287 + }
  288 +
  289 + /**
  290 + * 创建新客户 - 完整流程
  291 + * @param customerInfo 客户信息
  292 + * @param options 可选配置
  293 + */
  294 + async createCustomer(
  295 + customerInfo: {
  296 + name: string;
  297 + phone: string;
  298 + idCard: string;
  299 + detailedAddress?: string;
  300 + },
  301 + options?: {
  302 + creditLimit?: string;
  303 + licensePlate?: string;
  304 + province?: string;
  305 + city?: string;
  306 + district?: string;
  307 + imagePath?: string;
  308 + }
  309 + ): Promise<void> {
  310 + // 打开客户管理
  311 + await this.openCustomerManagement();
  312 +
  313 + // 点击新增客户
  314 + await this.clickAddCustomer();
  315 +
  316 + // 填写客户名称
  317 + await this.fillCustomerName(customerInfo.name);
  318 +
  319 + // 填写手机号
  320 + await this.fillPhone(customerInfo.phone);
  321 +
  322 + // 填写身份证
  323 + await this.fillIdCard(customerInfo.idCard);
  324 +
  325 + // 选择客户分组(默认第一个)
  326 + await this.selectCustomerGroup();
  327 +
  328 + // 如果有赊欠额度配置
  329 + if (options?.creditLimit) {
  330 + await this.enableCreditLimit();
  331 + await this.fillCreditLimit(options.creditLimit);
  332 + }
  333 +
  334 + // 如果有车牌号配置
  335 + if (options?.licensePlate) {
  336 + await this.fillLicensePlate(options.licensePlate);
  337 + }
  338 +
  339 + // 如果有省市区配置
  340 + if (options?.province && options?.city && options?.district) {
  341 + await this.selectRegion(options.province, options.city, options.district);
  342 + }
  343 +
  344 + // 如果有详细地址
  345 + if (customerInfo.detailedAddress) {
  346 + await this.fillDetailedAddress(customerInfo.detailedAddress);
  347 + }
  348 +
  349 + // 如果有图片上传
  350 + if (options?.imagePath) {
  351 + await this.uploadCustomerImage(options.imagePath);
  352 + }
  353 +
  354 + // 保存客户
  355 + await this.saveButton.click();
  356 +
  357 + // 等待保存完成
  358 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  359 + }
  360 +
  361 + /**
  362 + * 验证客户是否创建成功
  363 + * @param customerName 客户名称
  364 + */
  365 + async verifyCustomerCreated(customerName: string): Promise<boolean> {
  366 + try {
  367 + await this.page.waitForSelector(selectors.customerInList(customerName), { timeout: 10000 });
  368 + return true;
  369 + } catch {
  370 + return false;
  371 + }
  372 + }
  373 +}
0 374 \ No newline at end of file
... ...
test-data/img/海拉鲁.jpg 0 → 100644

7.87 KB

test-data/img/海拉鲁鲈鱼.jpg 0 → 100644

29.4 KB

test-data/img/牛肉.jpg 0 → 100644

20 KB

test-data/img/猪肉.jpg 0 → 100644

12.1 KB

test-data/img/苹果.jpg 0 → 100644

10.3 KB

test-data/img/莫利布林.jpg 0 → 100644

75.7 KB

test-data/img/角.jpg 0 → 100644

11.8 KB

test-data/img/鸭子.jpg 0 → 100644

16.6 KB

tests/consignmentOrder.spec.ts
... ... @@ -34,57 +34,8 @@ test.describe(&#39;代销入库&#39;, () =&gt; {
34 34 );
35 35 await consignmentPage.attachScreenshot(testInfo,'代销入库成功截图');
36 36 })
37   - // 使用页面对象创建代销入库
38   - // await consignmentPage.createConsignmentOrder(
39   - // batchAlias, // 批次别名
40   - // '娃娃菜', // 商品名称
41   - // '10', // 数量
42   - // '1' // 费用金额
43   - // );
  37 +
44 38 });
45 39  
46   - // test('新增代销入库 - 简化流程(无费用)', async ({ consignmentPage }) => {
47   - // // 生成唯一批次别名
48   - // const batchAlias = generateOtherName('代卖');
49   - // console.log('生成的批次别名:', batchAlias);
50 40  
51   - // // 使用简化流程创建代销入库
52   - // await consignmentPage.createSimpleConsignmentOrder(
53   - // batchAlias, // 批次别名
54   - // '娃娃菜' // 商品名称
55   - // );
56   -
57   - // // 验证批次创建成功
58   - // await consignmentPage.expectBatchCreated(batchAlias);
59   - // });
60   -
61   - // test('新增代销入库 - 分步操作示例', async ({ consignmentPage }) => {
62   - // // 生成唯一批次别名
63   - // const batchAlias = generateOtherName('代卖');
64   - // console.log('生成的批次别名:', batchAlias);
65   -
66   - // // 导航到新增页面
67   - // await consignmentPage.navigateToNewConsignment();
68   -
69   - // // 选择仓库
70   - // await consignmentPage.selectWarehouse();
71   - // await consignmentPage.selectSecondWarehouse();
72   -
73   - // // 输入批次别名
74   - // await consignmentPage.enterBatchAlias(batchAlias);
75   -
76   - // // 选择商品并输入数量
77   - // await consignmentPage.selectProductWithQuantity('娃娃菜', '10');
78   -
79   - // // 添加费用
80   - // await consignmentPage.addExpense(0, '1');
81   - // await consignmentPage.selectPaymentMethod(0);
82   -
83   - // // 点击创建
84   - // await consignmentPage.clickCreate();
85   - // await consignmentPage.waitForCreationComplete();
86   -
87   - // // 验证
88   - // await consignmentPage.expectBatchCreated(batchAlias);
89   - // });
90 41 });
91 42 \ No newline at end of file
... ...
tests/customer.spec.ts 0 → 100644
  1 +import { test, expect } from '../fixtures';
  2 +import { generateCustomerName, generateIdCard, generatePhoneNumber, generateDetailedAddress, generateCustomerInfo, getRandomImage } from '../utils/dataGenerator';
  3 +import * as allure from 'allure-js-commons';
  4 +
  5 +/**
  6 + * 客户管理测试
  7 + */
  8 +test.describe('客户管理', () => {
  9 + // 使用已保存的认证状态
  10 + test.use({ storageState: 'auth.json' });
  11 +
  12 + test('新增客户', async ({ customerPage }, testInfo) => {
  13 + // 添加allure元素
  14 + await allure.epic('客户管理');
  15 + await allure.feature('客户信息');
  16 + await allure.story('创建新客户');
  17 +
  18 + // 步骤1:生成随机客户信息
  19 + const customerInfo = await allure.step('生成随机客户信息', async (step) => {
  20 + const name = generateCustomerName();
  21 + const phone = generatePhoneNumber();
  22 + const idCard = generateIdCard();
  23 + const detailedAddress = generateDetailedAddress();
  24 +
  25 +
  26 + console.log('生成的客户信息:', { name, phone, idCard, detailedAddress });
  27 +
  28 + return { name, phone, idCard, detailedAddress };
  29 + });
  30 +
  31 + // 步骤2:执行新增客户操作
  32 + await allure.step('填写并提交客户表单', async () => {
  33 + await customerPage.gotoHome();
  34 +
  35 + // 从 test-data/img 目录随机选择一张图片
  36 + const randomImage = getRandomImage();
  37 +
  38 + await customerPage.createCustomer(
  39 + {
  40 + name: customerInfo.name,
  41 + phone: customerInfo.phone,
  42 + idCard: customerInfo.idCard,
  43 + detailedAddress: customerInfo.detailedAddress,
  44 + },
  45 + {
  46 + creditLimit: '500',
  47 + licensePlate: '渝ZY0706',
  48 + province: '江苏省',
  49 + city: '连云港市',
  50 + district: '海州区',
  51 + imagePath: randomImage || undefined, // 如果有图片则上传
  52 + }
  53 + );
  54 + await customerPage.attachScreenshot(testInfo, '新增客户成功截图');
  55 + });
  56 +
  57 + // 步骤3:验证客户创建成功
  58 + await allure.step('验证客户创建成功', async () => {
  59 + const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name);
  60 + expect(isCreated).toBeTruthy();
  61 + });
  62 + });
  63 +
  64 +// test('新增客户 - 仅必填信息', async ({ customerPage }, testInfo) => {
  65 +// // 添加allure元素
  66 +// await allure.epic('客户管理');
  67 +// await allure.feature('客户信息');
  68 +// await allure.story('创建新客户(仅必填信息)');
  69 +
  70 +// // 步骤1:生成随机客户信息
  71 +// const customerInfo = await allure.step('生成随机客户信息', async (step) => {
  72 +// const info = generateCustomerInfo();
  73 +// console.log('生成的客户信息:', info);
  74 +// return info;
  75 +// });
  76 +
  77 +// // 步骤2:执行新增客户操作
  78 +// await allure.step('填写并提交客户表单(仅必填信息)', async () => {
  79 +// await customerPage.gotoHome();
  80 +// await customerPage.createCustomer({
  81 +// name: customerInfo.name,
  82 +// phone: customerInfo.phone,
  83 +// idCard: customerInfo.idCard,
  84 +// });
  85 +// await customerPage.attachScreenshot(testInfo, '新增客户成功截图');
  86 +// });
  87 +
  88 +// // 步骤3:验证客户创建成功
  89 +// await allure.step('验证客户创建成功', async () => {
  90 +// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name);
  91 +// expect(isCreated).toBeTruthy();
  92 +// });
  93 +// });
  94 +
  95 +// test('新增客户 - 带详细地址', async ({ customerPage }, testInfo) => {
  96 +// // 添加allure元素
  97 +// await allure.epic('客户管理');
  98 +// await allure.feature('客户信息');
  99 +// await allure.story('创建新客户(带详细地址)');
  100 +
  101 +// // 步骤1:生成随机客户信息
  102 +// const customerInfo = await allure.step('生成随机客户信息', async (step) => {
  103 +// const name = generateCustomerName();
  104 +// const phone = generatePhoneNumber();
  105 +// const idCard = generateIdCard();
  106 +// const detailedAddress = generateDetailedAddress();
  107 +
  108 +// console.log('生成的客户信息:', { name, phone, idCard, detailedAddress });
  109 +
  110 +// return { name, phone, idCard, detailedAddress };
  111 +// });
  112 +
  113 +// // 步骤2:执行新增客户操作
  114 +// await allure.step('填写并提交客户表单', async () => {
  115 +// await customerPage.gotoHome();
  116 +// await customerPage.createCustomer(
  117 +// {
  118 +// name: customerInfo.name,
  119 +// phone: customerInfo.phone,
  120 +// idCard: customerInfo.idCard,
  121 +// },
  122 +// {
  123 +// creditLimit: '10000',
  124 +// }
  125 +// );
  126 +// await customerPage.attachScreenshot(testInfo, '新增客户成功截图');
  127 +// });
  128 +
  129 +// // 步骤3:验证客户创建成功
  130 +// await allure.step('验证客户创建成功', async () => {
  131 +// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name);
  132 +// expect(isCreated).toBeTruthy();
  133 +// });
  134 +// });
  135 +});
0 136 \ No newline at end of file
... ...
utils/dataGenerator.ts
  1 +import * as fs from 'fs';
  2 +import * as path from 'path';
  3 +
1 4 //商品管理-随机商品名
2 5 export function generateUniqueProductName(baseName: string = '测试商品'): string {
3 6 // 使用时间戳,精确到毫秒,保证每次都不一样
... ... @@ -14,4 +17,159 @@ export function generateOtherName(baseName: string = &#39;批次别名&#39;): string {
14 17 // 再加一个随机数,防止同一毫秒内多次调用
15 18 // const random = Math.floor(Math.random() * 100);
16 19 return `${baseName}${timestamp}`;
17   -}
18 20 \ No newline at end of file
  21 +}
  22 +
  23 +// 随机生成中文客户名称
  24 +export function generateCustomerName(): string {
  25 + const familyNames = ['张', '王', '李', '刘', '陈', '杨', '黄', '赵', '周', '吴', '徐', '孙', '马', '朱', '胡', '郭', '何', '林', '罗', '高'];
  26 + const givenNames = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '涛', '明', '超', '秀英', '华', '平', '刚', '桂英', '文', '志强', '建华', '建国', '建军', '晓明', '小红', '建军', '婷婷', '秀兰', '伟', '芳', '军', '勇', '艳', '杰', '娟', '涛', '明', '超', '秀英', '华'];
  27 +
  28 + const familyName = familyNames[Math.floor(Math.random() * familyNames.length)];
  29 + const givenName = givenNames[Math.floor(Math.random() * givenNames.length)];
  30 +
  31 + return familyName + givenName;
  32 +}
  33 +
  34 +// 随机生成身份证号码(18位)
  35 +export function generateIdCard(): string {
  36 + // 省份代码
  37 + const provinceCodes = ['11', '12', '13', '14', '15', '21', '22', '23', '31', '32', '33', '34', '35', '36', '37', '41', '42', '43', '44', '45', '46', '50', '51', '52', '53', '54', '61', '62', '63', '64', '65'];
  38 +
  39 + // 随机选择省份
  40 + const provinceCode = provinceCodes[Math.floor(Math.random() * provinceCodes.length)];
  41 +
  42 + // 随机生成城市和区县代码(各2位)
  43 + const cityCode = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');
  44 + const districtCode = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0');
  45 +
  46 + // 随机生成出生日期(1950-01-01 到 2005-12-31)
  47 + const startYear = 1950;
  48 + const endYear = 2005;
  49 + const year = Math.floor(Math.random() * (endYear - startYear + 1)) + startYear;
  50 + const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
  51 + const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0');
  52 + const birthDate = `${year}${month}${day}`;
  53 +
  54 + // 随机生成顺序码(3位)
  55 + const sequenceCode = String(Math.floor(Math.random() * 999)).padStart(3, '0');
  56 +
  57 + // 前17位
  58 + const id17 = provinceCode + cityCode + districtCode + birthDate + sequenceCode;
  59 +
  60 + // 计算校验码
  61 + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
  62 + const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
  63 +
  64 + let sum = 0;
  65 + for (let i = 0; i < 17; i++) {
  66 + sum += parseInt(id17[i]) * weights[i];
  67 + }
  68 + const checkCode = checkCodes[sum % 11];
  69 +
  70 + return id17 + checkCode;
  71 +}
  72 +
  73 +// 隨机生成手机号码
  74 +export function generatePhoneNumber(): string {
  75 + // 中国移动号段
  76 + const mobilePrefixes = ['134', '135', '136', '137', '138', '139', '147', '150', '151', '152', '157', '158', '159', '178', '182', '183', '184', '187', '188'];
  77 + // 中国联通号段
  78 + const unicomPrefixes = ['130', '131', '132', '145', '155', '156', '175', '176', '185', '186'];
  79 + // 中国电信号段
  80 + const telecomPrefixes = ['133', '149', '153', '177', '180', '181', '189'];
  81 +
  82 + const allPrefixes = [...mobilePrefixes, ...unicomPrefixes, ...telecomPrefixes];
  83 +
  84 + const prefix = allPrefixes[Math.floor(Math.random() * allPrefixes.length)];
  85 + const suffix = String(Math.floor(Math.random() * 100000000)).padStart(8, '0');
  86 +
  87 + return prefix + suffix;
  88 +}
  89 +
  90 +// 随机生成详细地址(街道、门牌号等,不含省市区)
  91 +export function generateDetailedAddress(): string {
  92 + const streets = ['中山路', '人民路', '建设路', '解放路', '和平路', '幸福路', '文化路', '科技路', '商业街', '繁华大道', '朝阳路', '胜利路', '长江路', '黄河路', '振兴路', '发展大道', '创业路', '创新路', '东风路', '西山路'];
  93 + const communities = ['阳光小区', '幸福花园', '和谐家园', '金色年华', '锦绣华庭', '碧水蓝天', '盛世华城', '锦绣江南', '紫荆花园', '翠竹苑', '莲花小区', '牡丹园', '玫瑰园', '百合苑', '茉莉花园'];
  94 + const buildings = ['1栋', '2栋', '3栋', '5栋', '6栋', '8栋', '9栋', '10栋', 'A座', 'B座', 'C座', 'D座'];
  95 + const units = ['1单元', '2单元', '3单元', '一单元', '二单元', '三单元'];
  96 + const floors = ['1楼', '2楼', '3楼', '5楼', '6楼', '8楼', '10楼', '12楼', '15楼', '18楼', '20楼'];
  97 + const rooms = ['101室', '201室', '301室', '401室', '501室', '601室', '701室', '801室', '1001室', '1101室', '1201室'];
  98 +
  99 + const street = streets[Math.floor(Math.random() * streets.length)];
  100 + const community = communities[Math.floor(Math.random() * communities.length)];
  101 + const building = buildings[Math.floor(Math.random() * buildings.length)];
  102 + const unit = units[Math.floor(Math.random() * units.length)];
  103 + const floor = floors[Math.floor(Math.random() * floors.length)];
  104 + const room = rooms[Math.floor(Math.random() * rooms.length)];
  105 +
  106 + // 随机组合地址格式
  107 + const formats = [
  108 + `${street}${Math.floor(Math.random() * 999) + 1}号`,
  109 + `${street}${Math.floor(Math.random() * 999) + 1}号${community}`,
  110 + `${community}${building}${unit}${room}`,
  111 + `${street}${Math.floor(Math.random() * 999) + 1}号${community}${building}${room}`,
  112 + `${street}${Math.floor(Math.random() * 999) + 1}号${floor}${room}`,
  113 + `${community}${building}${unit}${floor}${room}`
  114 + ];
  115 +
  116 + return formats[Math.floor(Math.random() * formats.length)];
  117 +}
  118 +
  119 +// 生成完整的客户信息对象
  120 +export function generateCustomerInfo(): {
  121 + name: string;
  122 + idCard: string;
  123 + phone: string;
  124 + detailedAddress: string;
  125 +} {
  126 + return {
  127 + name: generateCustomerName(),
  128 + idCard: generateIdCard(),
  129 + phone: generatePhoneNumber(),
  130 + detailedAddress: generateDetailedAddress()
  131 + };
  132 +}
  133 +
  134 +/**
  135 + * 从指定目录随机选择一张图片
  136 + * @param imageDir 图片目录路径,默认为 'test-data/img'
  137 + * @returns 随机选择的图片完整路径,如果目录不存在或没有图片则返回 null
  138 + */
  139 +export function getRandomImage(imageDir: string = 'test-data/img'): string | null {
  140 + try {
  141 + // 获取项目根目录
  142 + const projectRoot = process.cwd();
  143 + const fullImagePath = path.join(projectRoot, imageDir);
  144 +
  145 + // 检查目录是否存在
  146 + if (!fs.existsSync(fullImagePath)) {
  147 + console.warn(`图片目录不存在: ${fullImagePath}`);
  148 + return null;
  149 + }
  150 +
  151 + // 读取目录中的所有文件
  152 + const files = fs.readdirSync(fullImagePath);
  153 +
  154 + // 过滤出图片文件(支持常见图片格式)
  155 + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
  156 + const imageFiles = files.filter(file => {
  157 + const ext = path.extname(file).toLowerCase();
  158 + return imageExtensions.includes(ext);
  159 + });
  160 +
  161 + if (imageFiles.length === 0) {
  162 + console.warn(`图片目录中没有图片文件: ${fullImagePath}`);
  163 + return null;
  164 + }
  165 +
  166 + // 随机选择一张图片
  167 + const randomIndex = Math.floor(Math.random() * imageFiles.length);
  168 + const selectedImage = imageFiles[randomIndex];
  169 +
  170 + // 返回相对于项目根目录的路径
  171 + return path.join(imageDir, selectedImage);
  172 + } catch (error) {
  173 + console.error('获取随机图片失败:', error);
  174 + return null;
  175 + }
  176 +}
... ...