Commit 0247a9d4c2c7f49774781a91b49386ce84ad317e
1 parent
33bc2f2f
新增客户管理和客户分组主流程
Showing
8 changed files
with
739 additions
and
271 deletions
pages/basePage.ts
| @@ -318,7 +318,21 @@ export abstract class BasePage { | @@ -318,7 +318,21 @@ export abstract class BasePage { | ||
| 318 | if (uploadButtonLocator) { | 318 | if (uploadButtonLocator) { |
| 319 | await uploadButtonLocator.click(); | 319 | await uploadButtonLocator.click(); |
| 320 | } else { | 320 | } else { |
| 321 | - await this.page.locator('uni-scroll-view uni-button').click(); | 321 | + // 尝试多种方式触发文件选择器 |
| 322 | + try { | ||
| 323 | + // 方法1: 查找上传按钮(包含图片相关的按钮) | ||
| 324 | + const uploadBtn = this.page.locator('.nut-uploader__input').first(); | ||
| 325 | + await uploadBtn.click({ timeout: 3000 }); | ||
| 326 | + } catch { | ||
| 327 | + // 方法2: 直接在文件输入框上设置文件 | ||
| 328 | + const fileInput = this.page.locator('input[type="file"]'); | ||
| 329 | + if (await fileInput.count() > 0) { | ||
| 330 | + await fileInput.setInputFiles(imagePath); | ||
| 331 | + return; | ||
| 332 | + } | ||
| 333 | + // 方法3: 尝试点击文字 | ||
| 334 | + await this.page.getByText('点击上传').click({ timeout: 3000 }); | ||
| 335 | + } | ||
| 322 | } | 336 | } |
| 323 | 337 | ||
| 324 | // 等待文件选择器出现并设置文件 | 338 | // 等待文件选择器出现并设置文件 |
| @@ -340,4 +354,62 @@ export abstract class BasePage { | @@ -340,4 +354,62 @@ export abstract class BasePage { | ||
| 340 | // 等待上传完成 | 354 | // 等待上传完成 |
| 341 | await this.page.waitForTimeout(1000); | 355 | await this.page.waitForTimeout(1000); |
| 342 | } | 356 | } |
| 357 | + | ||
| 358 | + /** | ||
| 359 | + * 选择省市区(通用方法) | ||
| 360 | + * @param regionPickerLocator 省市区选择器定位器 | ||
| 361 | + * @param province 省份(如:山西省) | ||
| 362 | + * @param city 城市(如:长治市) | ||
| 363 | + * @param district 区县(如:潞城区) | ||
| 364 | + * @param confirmButtonText 确认按钮文本(默认:确认选择) | ||
| 365 | + */ | ||
| 366 | + async selectRegion( | ||
| 367 | + regionPickerLocator: Locator, | ||
| 368 | + province: string, | ||
| 369 | + city: string, | ||
| 370 | + district: string, | ||
| 371 | + confirmButtonText: string = '确认选择' | ||
| 372 | + ): Promise<void> { | ||
| 373 | + // 点击省市区选择器 | ||
| 374 | + await regionPickerLocator.click(); | ||
| 375 | + | ||
| 376 | + // 等待弹窗出现 | ||
| 377 | + await this.page.waitForTimeout(500); | ||
| 378 | + | ||
| 379 | + // 选择省份 - 使用exact选项避免匹配到完整地址 | ||
| 380 | + await this.page.getByText(province, { exact: true }).click(); | ||
| 381 | + | ||
| 382 | + // 选择城市 - 使用exact选项 | ||
| 383 | + await this.page.getByText(city, { exact: true }).click(); | ||
| 384 | + | ||
| 385 | + // 选择区县 - 使用exact选项 | ||
| 386 | + await this.page.getByText(district, { exact: true }).click(); | ||
| 387 | + | ||
| 388 | + // 确认选择 | ||
| 389 | + await this.page.getByText(confirmButtonText).click(); | ||
| 390 | + } | ||
| 391 | + | ||
| 392 | + /** | ||
| 393 | + * 选择省市区(通过选择器字符串) | ||
| 394 | + * @param regionPickerSelector 省市区选择器选择器字符串 | ||
| 395 | + * @param province 省份(如:山西省) | ||
| 396 | + * @param city 城市(如:长治市) | ||
| 397 | + * @param district 区县(如:潞城区) | ||
| 398 | + * @param confirmButtonText 确认按钮文本(默认:确认选择) | ||
| 399 | + */ | ||
| 400 | + async selectRegionBySelector( | ||
| 401 | + regionPickerSelector: string, | ||
| 402 | + province: string, | ||
| 403 | + city: string, | ||
| 404 | + district: string, | ||
| 405 | + confirmButtonText: string = '确认选择' | ||
| 406 | + ): Promise<void> { | ||
| 407 | + await this.selectRegion( | ||
| 408 | + this.page.locator(regionPickerSelector), | ||
| 409 | + province, | ||
| 410 | + city, | ||
| 411 | + district, | ||
| 412 | + confirmButtonText | ||
| 413 | + ); | ||
| 414 | + } | ||
| 343 | } | 415 | } |
pages/customerPage.ts
| @@ -94,7 +94,7 @@ export class CustomerPage extends BasePage { | @@ -94,7 +94,7 @@ export class CustomerPage extends BasePage { | ||
| 94 | this.confirmRegionButton = page.getByText('确认选择'); | 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'); | 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('确定'); | 96 | this.confirmButton = page.getByText('确定'); |
| 97 | - this.saveButton = page.getByText('保存'); | 97 | + this.saveButton = page.locator('uni-button.execute-btn').filter({ hasText: '保存' }); |
| 98 | } | 98 | } |
| 99 | 99 | ||
| 100 | /** | 100 | /** |
| @@ -203,6 +203,27 @@ export class CustomerPage extends BasePage { | @@ -203,6 +203,27 @@ export class CustomerPage extends BasePage { | ||
| 203 | } | 203 | } |
| 204 | 204 | ||
| 205 | /** | 205 | /** |
| 206 | + * 检查赊欠额度开关是否已开启 | ||
| 207 | + * @returns 是否已开启 | ||
| 208 | + */ | ||
| 209 | + async isCreditLimitEnabled(): Promise<boolean> { | ||
| 210 | + // 检查开关的激活状态(nut-switch 组件在激活时会有 nut-switch--active 类) | ||
| 211 | + const switchElement = this.creditLimitSwitch; | ||
| 212 | + const className = await switchElement.getAttribute('class'); | ||
| 213 | + return className?.includes('nut-switch--active') ?? false; | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + /** | ||
| 217 | + * 确保赊欠额度开关已开启,如果未开启则点击开启 | ||
| 218 | + */ | ||
| 219 | + async ensureCreditLimitEnabled(): Promise<void> { | ||
| 220 | + const isEnabled = await this.isCreditLimitEnabled(); | ||
| 221 | + if (!isEnabled) { | ||
| 222 | + await this.enableCreditLimit(); | ||
| 223 | + } | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + /** | ||
| 206 | * 填写赊欠额度 | 227 | * 填写赊欠额度 |
| 207 | * @param amount 赊欠额度金额 | 228 | * @param amount 赊欠额度金额 |
| 208 | */ | 229 | */ |
| @@ -229,24 +250,12 @@ export class CustomerPage extends BasePage { | @@ -229,24 +250,12 @@ export class CustomerPage extends BasePage { | ||
| 229 | * @param city 城市(如:长治市) | 250 | * @param city 城市(如:长治市) |
| 230 | * @param district 区县(如:潞城区) | 251 | * @param district 区县(如:潞城区) |
| 231 | */ | 252 | */ |
| 232 | - async selectRegion(province: string, city: string, district: string): Promise<void> { | 253 | + async selectCustomerRegion(province: string, city: string, district: string): Promise<void> { |
| 233 | // 点击省市区选择器 - nth-child(12),开启赊欠额度后位置 | 254 | // 点击省市区选择器 - nth-child(12),开启赊欠额度后位置 |
| 234 | - 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(); | ||
| 235 | - | ||
| 236 | - // 等待弹窗出现 | ||
| 237 | - await this.page.waitForTimeout(500); | 255 | + const regionPickerLocator = 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'); |
| 238 | 256 | ||
| 239 | - // 选择省份 | ||
| 240 | - await this.page.getByText(province).click(); | ||
| 241 | - | ||
| 242 | - // 选择城市 | ||
| 243 | - await this.page.getByText(city).click(); | ||
| 244 | - | ||
| 245 | - // 选择区县 | ||
| 246 | - await this.page.getByText(district).click(); | ||
| 247 | - | ||
| 248 | - // 确认选择 | ||
| 249 | - await this.page.getByText('确认选择').click(); | 257 | + // 调用基类的通用方法 |
| 258 | + await super.selectRegion(regionPickerLocator, province, city, district); | ||
| 250 | } | 259 | } |
| 251 | 260 | ||
| 252 | /** | 261 | /** |
| @@ -321,7 +330,7 @@ export class CustomerPage extends BasePage { | @@ -321,7 +330,7 @@ export class CustomerPage extends BasePage { | ||
| 321 | 330 | ||
| 322 | // 如果有省市区配置 | 331 | // 如果有省市区配置 |
| 323 | if (options?.province && options?.city && options?.district) { | 332 | if (options?.province && options?.city && options?.district) { |
| 324 | - await this.selectRegion(options.province, options.city, options.district); | 333 | + await this.selectCustomerRegion(options.province, options.city, options.district); |
| 325 | } | 334 | } |
| 326 | 335 | ||
| 327 | // 如果有详细地址 | 336 | // 如果有详细地址 |
| @@ -347,6 +356,21 @@ export class CustomerPage extends BasePage { | @@ -347,6 +356,21 @@ export class CustomerPage extends BasePage { | ||
| 347 | */ | 356 | */ |
| 348 | async verifyCustomerCreated(customerName: string): Promise<boolean> { | 357 | async verifyCustomerCreated(customerName: string): Promise<boolean> { |
| 349 | try { | 358 | try { |
| 359 | + // 先返回首页确保在客户列表页面 | ||
| 360 | + await this.gotoHome(); | ||
| 361 | + await this.openCustomerManagement(); | ||
| 362 | + await this.page.waitForLoadState('networkidle'); | ||
| 363 | + await this.page.waitForTimeout(500); | ||
| 364 | + | ||
| 365 | + // 清空搜索框(点击清除图标) | ||
| 366 | + const clearBtn = this.page.locator('.nut-icon-circle-close'); | ||
| 367 | + if (await clearBtn.isVisible()) { | ||
| 368 | + await clearBtn.click(); | ||
| 369 | + await this.page.waitForTimeout(500); | ||
| 370 | + } | ||
| 371 | + | ||
| 372 | + // 搜索客户 | ||
| 373 | + await this.searchCustomer(customerName); | ||
| 350 | await this.page.waitForSelector(selectors.customerInList(customerName), { timeout: 10000 }); | 374 | await this.page.waitForSelector(selectors.customerInList(customerName), { timeout: 10000 }); |
| 351 | return true; | 375 | return true; |
| 352 | } catch { | 376 | } catch { |
| @@ -364,7 +388,12 @@ export class CustomerPage extends BasePage { | @@ -364,7 +388,12 @@ export class CustomerPage extends BasePage { | ||
| 364 | // 点击搜索框区域(使用 force: true 绕过遮罩层) | 388 | // 点击搜索框区域(使用 force: true 绕过遮罩层) |
| 365 | await this.page.locator('uni-view').filter({ hasText: /^客户名称\/手机号$/ }).nth(2).click({ force: true }); | 389 | await this.page.locator('uni-view').filter({ hasText: /^客户名称\/手机号$/ }).nth(2).click({ force: true }); |
| 366 | // 在输入框中填写客户名称 | 390 | // 在输入框中填写客户名称 |
| 367 | - await this.page.getByRole('textbox').fill(customerName); | 391 | + const input = this.page.locator('uni-input').filter({ hasText: '客户名称/手机号' }).getByRole('textbox'); |
| 392 | + await input.fill(customerName); | ||
| 393 | + // 等待搜索结果出现 | ||
| 394 | + await this.page.waitForTimeout(1500); | ||
| 395 | + // 按回车键确认搜索 | ||
| 396 | + await input.press('Enter'); | ||
| 368 | await this.page.waitForTimeout(1000); | 397 | await this.page.waitForTimeout(1000); |
| 369 | } | 398 | } |
| 370 | 399 | ||
| @@ -373,7 +402,10 @@ export class CustomerPage extends BasePage { | @@ -373,7 +402,10 @@ export class CustomerPage extends BasePage { | ||
| 373 | * @param customerName 客户名称 | 402 | * @param customerName 客户名称 |
| 374 | */ | 403 | */ |
| 375 | async clickCustomerItem(customerName: string): Promise<void> { | 404 | async clickCustomerItem(customerName: string): Promise<void> { |
| 376 | - await this.page.getByText(customerName).first().click(); | 405 | + // 使用Playwright的locator语法查找搜索结果列表中的客户项 |
| 406 | + const customerItem = this.page.locator('uni-view').filter({ hasText: customerName }).first(); | ||
| 407 | + await customerItem.waitFor({ state: 'visible', timeout: 10000 }); | ||
| 408 | + await customerItem.click(); | ||
| 377 | await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | 409 | await this.page.waitForLoadState('networkidle', { timeout: 30000 }); |
| 378 | } | 410 | } |
| 379 | 411 | ||
| @@ -438,9 +470,10 @@ export class CustomerPage extends BasePage { | @@ -438,9 +470,10 @@ export class CustomerPage extends BasePage { | ||
| 438 | await this.clearAndFillIdCard(customerInfo.idCard); | 470 | await this.clearAndFillIdCard(customerInfo.idCard); |
| 439 | 471 | ||
| 440 | if (options?.creditLimit) { | 472 | if (options?.creditLimit) { |
| 441 | - // 清除并填写赊欠额度(使用 force: true 绕过遮罩层) | 473 | + // 先确保赊欠额度开关已开启 |
| 474 | + await this.ensureCreditLimitEnabled(); | ||
| 475 | + // 直接填写赊欠额度(fill会自动清除) | ||
| 442 | await this.creditLimitInput.click({ force: true }); | 476 | await this.creditLimitInput.click({ force: true }); |
| 443 | - await this.page.locator('uni-view:nth-child(8) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__clear > .nut-input__clear-icon').click({ force: true }); | ||
| 444 | await this.creditLimitInput.fill(options.creditLimit); | 477 | await this.creditLimitInput.fill(options.creditLimit); |
| 445 | } | 478 | } |
| 446 | 479 | ||
| @@ -449,7 +482,7 @@ export class CustomerPage extends BasePage { | @@ -449,7 +482,7 @@ export class CustomerPage extends BasePage { | ||
| 449 | } | 482 | } |
| 450 | 483 | ||
| 451 | if (options?.province && options?.city && options?.district) { | 484 | if (options?.province && options?.city && options?.district) { |
| 452 | - await this.selectRegion(options.province, options.city, options.district); | 485 | + await this.selectCustomerRegion(options.province, options.city, options.district); |
| 453 | } | 486 | } |
| 454 | 487 | ||
| 455 | if (customerInfo.detailedAddress) { | 488 | if (customerInfo.detailedAddress) { |
| @@ -476,6 +509,9 @@ export class CustomerPage extends BasePage { | @@ -476,6 +509,9 @@ export class CustomerPage extends BasePage { | ||
| 476 | await this.clickCustomerItem(customerName); | 509 | await this.clickCustomerItem(customerName); |
| 477 | await this.clickDeleteButton(); | 510 | await this.clickDeleteButton(); |
| 478 | await this.confirmDelete(); | 511 | await this.confirmDelete(); |
| 512 | + // 点击确定后,页面会返回到列表页,等待加载完成 | ||
| 513 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 514 | + await this.page.waitForTimeout(1000); | ||
| 479 | } | 515 | } |
| 480 | 516 | ||
| 481 | /** | 517 | /** |
| @@ -485,59 +521,30 @@ export class CustomerPage extends BasePage { | @@ -485,59 +521,30 @@ export class CustomerPage extends BasePage { | ||
| 485 | async verifyCustomerDeleted(customerName: string): Promise<boolean> { | 521 | async verifyCustomerDeleted(customerName: string): Promise<boolean> { |
| 486 | await this.gotoHome(); | 522 | await this.gotoHome(); |
| 487 | await this.openCustomerManagement(); | 523 | await this.openCustomerManagement(); |
| 524 | + await this.page.waitForLoadState('networkidle'); | ||
| 525 | + await this.page.waitForTimeout(500); | ||
| 526 | + | ||
| 527 | + // 清空搜索框(点击清除图标) | ||
| 528 | + const clearBtn = this.page.locator('.nut-icon-circle-close'); | ||
| 529 | + if (await clearBtn.isVisible()) { | ||
| 530 | + await clearBtn.click(); | ||
| 531 | + await this.page.waitForTimeout(500); | ||
| 532 | + } | ||
| 533 | + | ||
| 488 | await this.searchCustomer(customerName); | 534 | await this.searchCustomer(customerName); |
| 535 | + await this.page.waitForTimeout(1000); | ||
| 489 | 536 | ||
| 490 | - const count = await this.page.getByText(customerName).count(); | 537 | + // 在客户列表中查找(只在 .nut-cell-group 或列表容器中查找) |
| 538 | + const listItem = this.page.locator('.nut-cell-group__wrapper').locator('.nut-cell', { hasText: customerName }); | ||
| 539 | + const count = await listItem.count(); | ||
| 540 | + console.log(`查找 "${customerName}" 在列表中找到 ${count} 个匹配`); | ||
| 491 | return count === 0; | 541 | return count === 0; |
| 492 | } | 542 | } |
| 493 | 543 | ||
| 494 | // ==================== 录入欠款相关方法 ==================== | 544 | // ==================== 录入欠款相关方法 ==================== |
| 495 | 545 | ||
| 496 | /** | 546 | /** |
| 497 | - * 点击录入欠款按钮 | ||
| 498 | - */ | ||
| 499 | - async clickRecordDebt(): Promise<void> { | ||
| 500 | - await this.page.getByText('录入欠款').click(); | ||
| 501 | - await this.page.waitForTimeout(500); | ||
| 502 | - } | ||
| 503 | - | ||
| 504 | - /** | ||
| 505 | - * 选择欠款类型 | ||
| 506 | - * @param typeIndex 类型索引(默认选择第三个,索引为2) | ||
| 507 | - */ | ||
| 508 | - async selectDebtType(typeIndex: number = 2): Promise<void> { | ||
| 509 | - await this.page.locator('uni-view:nth-child(4) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask').click(); | ||
| 510 | - await this.page.locator('uni-view').filter({ hasText: /^2$/ }).nth(typeIndex).click(); | ||
| 511 | - } | ||
| 512 | - | ||
| 513 | - /** | ||
| 514 | - * 填写欠款金额 | ||
| 515 | - * @param amount 欠款金额 | ||
| 516 | - */ | ||
| 517 | - async fillDebtAmount(amount: string): Promise<void> { | ||
| 518 | - await this.page.getByRole('spinbutton').click(); | ||
| 519 | - await this.page.getByRole('spinbutton').fill(amount); | ||
| 520 | - } | ||
| 521 | - | ||
| 522 | - /** | ||
| 523 | - * 填写欠款备注 | ||
| 524 | - * @param remark 备注 | ||
| 525 | - */ | ||
| 526 | - async fillDebtRemark(remark: string): Promise<void> { | ||
| 527 | - await this.page.getByRole('textbox').nth(4).click(); | ||
| 528 | - await this.page.getByRole('textbox').nth(4).fill(remark); | ||
| 529 | - } | ||
| 530 | - | ||
| 531 | - /** | ||
| 532 | - * 保存欠款 | ||
| 533 | - */ | ||
| 534 | - async saveDebt(): Promise<void> { | ||
| 535 | - await this.page.getByText('保存', { exact: true }).click(); | ||
| 536 | - await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 537 | - } | ||
| 538 | - | ||
| 539 | - /** | ||
| 540 | - * 录入欠款 - 完整流程 | 547 | + * 录入欠款 - 完整流程(基于录制脚本重写) |
| 541 | * @param customerName 客户名称 | 548 | * @param customerName 客户名称 |
| 542 | * @param amount 欠款金额 | 549 | * @param amount 欠款金额 |
| 543 | * @param options 可选配置 | 550 | * @param options 可选配置 |
| @@ -546,12 +553,12 @@ export class CustomerPage extends BasePage { | @@ -546,12 +553,12 @@ export class CustomerPage extends BasePage { | ||
| 546 | customerName: string, | 553 | customerName: string, |
| 547 | amount: string, | 554 | amount: string, |
| 548 | options?: { | 555 | options?: { |
| 549 | - typeIndex?: number; // 欠款类型索引 | 556 | + debtType?: string; // 欠款类型(如 '15') |
| 550 | remark?: string; // 备注 | 557 | remark?: string; // 备注 |
| 551 | imagePath?: string; // 图片路径 | 558 | imagePath?: string; // 图片路径 |
| 552 | } | 559 | } |
| 553 | ): Promise<void> { | 560 | ): Promise<void> { |
| 554 | - // 先返回首页确保页面状态干净(避免遮罩层阻挡) | 561 | + // 先返回首页确保页面状态干净 |
| 555 | await this.gotoHome(); | 562 | await this.gotoHome(); |
| 556 | // 打开客户管理并搜索 | 563 | // 打开客户管理并搜索 |
| 557 | await this.openCustomerManagement(); | 564 | await this.openCustomerManagement(); |
| @@ -559,39 +566,79 @@ export class CustomerPage extends BasePage { | @@ -559,39 +566,79 @@ export class CustomerPage extends BasePage { | ||
| 559 | await this.clickCustomerItem(customerName); | 566 | await this.clickCustomerItem(customerName); |
| 560 | 567 | ||
| 561 | // 点击录入欠款 | 568 | // 点击录入欠款 |
| 562 | - await this.clickRecordDebt(); | 569 | + await this.page.getByText('录入欠款').click(); |
| 570 | + await this.page.waitForTimeout(500); | ||
| 563 | 571 | ||
| 564 | - // 选择欠款类型 | ||
| 565 | - if (options?.typeIndex !== undefined) { | ||
| 566 | - await this.selectDebtType(options.typeIndex); | ||
| 567 | - } | 572 | + // 选择欠款类型 - 点击下拉框 |
| 573 | + await this.page.locator('uni-view:nth-child(4) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask').click(); | ||
| 574 | + // 选择类型(默认使用 '15') | ||
| 575 | + const debtType = options?.debtType || '15'; | ||
| 576 | + await this.page.getByText(debtType, { exact: true }).click(); | ||
| 568 | 577 | ||
| 569 | // 填写欠款金额 | 578 | // 填写欠款金额 |
| 570 | - await this.fillDebtAmount(amount); | 579 | + await this.page.getByRole('spinbutton').click(); |
| 580 | + await this.page.getByRole('spinbutton').fill(amount); | ||
| 571 | 581 | ||
| 572 | // 填写备注 | 582 | // 填写备注 |
| 573 | if (options?.remark) { | 583 | if (options?.remark) { |
| 574 | - await this.fillDebtRemark(options.remark); | 584 | + await this.page.getByRole('textbox').nth(4).click(); |
| 585 | + await this.page.getByRole('textbox').nth(4).fill(options.remark); | ||
| 575 | } | 586 | } |
| 576 | 587 | ||
| 577 | - // 上传图片 - 使用基类封装的方法 | ||
| 578 | - if (options?.imagePath) { | ||
| 579 | - await this.uploadCustomerImage(options.imagePath); | ||
| 580 | - } | 588 | + // 上传图片(暂时跳过,如果需要可以后续启用) |
| 589 | + // if (options?.imagePath) { | ||
| 590 | + // // 使用基类的uploadImage方法上传图片 | ||
| 591 | + // await this.uploadImage(options.imagePath); | ||
| 592 | + // } | ||
| 581 | 593 | ||
| 582 | - // 保存 | ||
| 583 | - await this.saveDebt(); | 594 | + // 点击保存按钮 |
| 595 | + await this.page.getByText('保存', { exact: true }).click(); | ||
| 596 | + await this.page.waitForTimeout(2000); | ||
| 584 | } | 597 | } |
| 585 | 598 | ||
| 586 | /** | 599 | /** |
| 587 | * 验证欠款是否录入成功 | 600 | * 验证欠款是否录入成功 |
| 588 | - * @param amount 欠款金额 | 601 | + * 检查客户详情页的"当前欠款"字段是否显示正确金额 |
| 602 | + * @param expectedAmount 期望的欠款金额 | ||
| 589 | */ | 603 | */ |
| 590 | - async verifyDebtRecorded(amount: string): Promise<boolean> { | 604 | + async verifyDebtRecorded(expectedAmount: string): Promise<boolean> { |
| 591 | try { | 605 | try { |
| 592 | - await this.page.getByText(`欠款:${amount}.00元`).waitFor({ timeout: 10000 }); | ||
| 593 | - return true; | ||
| 594 | - } catch { | 606 | + // 等待页面稳定 |
| 607 | + await this.page.waitForTimeout(2000); | ||
| 608 | + await this.page.waitForLoadState('networkidle'); | ||
| 609 | + | ||
| 610 | + // 显示格式是"欠款:{金额}.00元",其中金额是原始输入值 | ||
| 611 | + const displayAmount = parseFloat(expectedAmount).toFixed(2); | ||
| 612 | + const searchText = `欠款:${displayAmount}元`; | ||
| 613 | + | ||
| 614 | + console.log(`查找欠款文本: ${searchText}`); | ||
| 615 | + | ||
| 616 | + // 直接查找包含"欠款:{金额}元"格式的元素 | ||
| 617 | + const debtElement = this.page.getByText(searchText); | ||
| 618 | + const isVisible = await debtElement.isVisible({ timeout: 5000 }).catch(() => false); | ||
| 619 | + | ||
| 620 | + if (isVisible) { | ||
| 621 | + console.log(`验证成功:找到欠款 ${searchText}`); | ||
| 622 | + return true; | ||
| 623 | + } | ||
| 624 | + | ||
| 625 | + // 备用方案:查找包含"欠款:"的元素 | ||
| 626 | + const debtWithLabel = this.page.locator('uni-view').filter({ hasText: /^欠款:/ }); | ||
| 627 | + const count = await debtWithLabel.count(); | ||
| 628 | + console.log(`找到 ${count} 个包含"欠款:"的元素`); | ||
| 629 | + | ||
| 630 | + if (count > 0) { | ||
| 631 | + const text = await debtWithLabel.first().textContent(); | ||
| 632 | + console.log(`欠款文本内容: ${text}`); | ||
| 633 | + if (text && text.includes(displayAmount)) { | ||
| 634 | + return true; | ||
| 635 | + } | ||
| 636 | + } | ||
| 637 | + | ||
| 638 | + console.log(`验证失败:未找到金额 ${expectedAmount}`); | ||
| 639 | + return false; | ||
| 640 | + } catch (error) { | ||
| 641 | + console.log('验证欠款失败:', error); | ||
| 595 | return false; | 642 | return false; |
| 596 | } | 643 | } |
| 597 | } | 644 | } |
scripts/save-auth.ts
| 1 | // 半自动登录,登录存放auth.json | 1 | // 半自动登录,登录存放auth.json |
| 2 | -import { test, expect } from '@playwright/test'; | 2 | +import { chromium } from '@playwright/test'; |
| 3 | +import dotenv from 'dotenv'; | ||
| 4 | +import path from 'path'; | ||
| 5 | + | ||
| 6 | +dotenv.config({ path: path.resolve(__dirname, '../.env') }); | ||
| 3 | 7 | ||
| 4 | -// 从环境变量获取配置 | ||
| 5 | const TEST_PHONE = process.env.phone; | 8 | const TEST_PHONE = process.env.phone; |
| 6 | const TEST_USER_NAME = process.env.TEST_USER_NAME; | 9 | const TEST_USER_NAME = process.env.TEST_USER_NAME; |
| 10 | +const BASE_URL = process.env.BASE_URL || 'https://erp-pad.test.gszdtop.com'; | ||
| 7 | 11 | ||
| 8 | -if (!TEST_PHONE) { | ||
| 9 | - throw new Error('phone 环境变量未设置,请设置 phone 后再运行'); | 12 | +if (!TEST_PHONE || !TEST_USER_NAME) { |
| 13 | + throw new Error('phone 和 TEST_USER_NAME 环境变量未设置'); | ||
| 10 | } | 14 | } |
| 11 | -if (!TEST_USER_NAME) { | ||
| 12 | - throw new Error('TEST_USER_NAME 环境变量未设置,请设置 TEST_USER_NAME 后再运行'); | 15 | + |
| 16 | +async function main() { | ||
| 17 | + console.log('开始半自动登录...'); | ||
| 18 | + | ||
| 19 | + const browser = await chromium.launch({ headless: false }); | ||
| 20 | + const context = await browser.newContext(); | ||
| 21 | + const page = await context.newPage(); | ||
| 22 | + | ||
| 23 | + try { | ||
| 24 | + await page.goto(`${BASE_URL}/#/pages/login/index`); | ||
| 25 | + await page.waitForLoadState('domcontentloaded'); | ||
| 26 | + | ||
| 27 | + // 已登录则直接保存 | ||
| 28 | + try { | ||
| 29 | + await page.getByText(TEST_USER_NAME!).waitFor({ state: 'visible', timeout: 3000 }); | ||
| 30 | + console.log('已登录,保存状态...'); | ||
| 31 | + await context.storageState({ path: 'auth.json' }); | ||
| 32 | + console.log('状态已保存到 auth.json'); | ||
| 33 | + return; | ||
| 34 | + } catch { | ||
| 35 | + console.log('未登录,开始登录...'); | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + // 手机号登录流程 | ||
| 39 | + await page.getByText('手机号登录/注册').click(); | ||
| 40 | + await page.getByText('确定').click(); | ||
| 41 | + | ||
| 42 | + // 填写手机号 | ||
| 43 | + const phoneInput = page.locator('uni-input').filter({ hasText: '请输入手机号' }).locator('input').first(); | ||
| 44 | + await phoneInput.click(); | ||
| 45 | + await phoneInput.fill(TEST_PHONE!); | ||
| 46 | + | ||
| 47 | + // 获取验证码 | ||
| 48 | + await page.getByText('获取验证码').click(); | ||
| 49 | + console.log('请手动输入验证码并点击登录...'); | ||
| 50 | + | ||
| 51 | + // 等待登录成功 | ||
| 52 | + await page.getByText(TEST_USER_NAME!).waitFor({ timeout: 120000 }); | ||
| 53 | + console.log('登录成功!'); | ||
| 54 | + | ||
| 55 | + await context.storageState({ path: 'auth.json' }); | ||
| 56 | + console.log('状态已保存到 auth.json'); | ||
| 57 | + | ||
| 58 | + } finally { | ||
| 59 | + await browser.close(); | ||
| 60 | + } | ||
| 13 | } | 61 | } |
| 14 | 62 | ||
| 15 | -test('半自动登录(自动填手机号,手动输验证码)', async ({ page }) => { | ||
| 16 | - await page.goto('/#/pages/login/index'); | ||
| 17 | - await page.getByText('手机号登录/注册').click(); | ||
| 18 | - await page.getByText('确定').click(); | ||
| 19 | - // --- 这里执行登录操作 --- | ||
| 20 | - await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').click(); | ||
| 21 | - await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').fill(TEST_PHONE); | ||
| 22 | - // 此处假设出现了验证码,你需要手动输入或处理 | ||
| 23 | - await page.locator('uni-button').filter({ hasText: '获取验证码' }).click(); | ||
| 24 | - console.log('请手动输入验证码,然后点击登录按钮...'); | ||
| 25 | - await page.locator('uni-button').filter({ hasText: '登录' }).click(); | ||
| 26 | - // --- 等待登录成功,跳转到首页 --- | ||
| 27 | - await expect(page.getByText(TEST_USER_NAME)).toBeVisible({ timeout: 0 }); | ||
| 28 | - | ||
| 29 | - // 4. 保存存储状态(Cookie 和 LocalStorage) | ||
| 30 | - await page.context().storageState({ path: 'auth.json' }); | ||
| 31 | - console.log('状态已保存到 auth.json'); | ||
| 32 | -}); | ||
| 33 | \ No newline at end of file | 63 | \ No newline at end of file |
| 64 | +main().catch(console.error); |
tests/customer.spec.ts
| @@ -9,6 +9,9 @@ import * as allure from 'allure-js-commons'; | @@ -9,6 +9,9 @@ import * as allure from 'allure-js-commons'; | ||
| 9 | test.describe('客户管理', () => { | 9 | test.describe('客户管理', () => { |
| 10 | // 使用已保存的认证状态 | 10 | // 使用已保存的认证状态 |
| 11 | test.use({ storageState: 'auth.json' }); | 11 | test.use({ storageState: 'auth.json' }); |
| 12 | + | ||
| 13 | + // 强制测试串行执行,避免并行测试之间的干扰 | ||
| 14 | + test.describe.configure({ mode: 'serial' }); | ||
| 12 | 15 | ||
| 13 | test('新增客户', async ({ customerPage }, testInfo) => { | 16 | test('新增客户', async ({ customerPage }, testInfo) => { |
| 14 | // 添加allure元素 | 17 | // 添加allure元素 |
| @@ -24,7 +27,7 @@ test.describe('客户管理', () => { | @@ -24,7 +27,7 @@ test.describe('客户管理', () => { | ||
| 24 | const detailedAddress = generateDetailedAddress(); | 27 | const detailedAddress = generateDetailedAddress(); |
| 25 | 28 | ||
| 26 | 29 | ||
| 27 | - console.log('生成的客户信息:', { name, phone, idCard, detailedAddress }); | 30 | + // console.log('生成的客户信息:', { name, phone, idCard, detailedAddress }); |
| 28 | 31 | ||
| 29 | return { name, phone, idCard, detailedAddress }; | 32 | return { name, phone, idCard, detailedAddress }; |
| 30 | }); | 33 | }); |
| @@ -62,145 +65,191 @@ test.describe('客户管理', () => { | @@ -62,145 +65,191 @@ test.describe('客户管理', () => { | ||
| 62 | }); | 65 | }); |
| 63 | }); | 66 | }); |
| 64 | 67 | ||
| 65 | -// test('修改客户', async ({ customerPage }, testInfo) => { | ||
| 66 | -// // 添加allure元素 | ||
| 67 | -// await allure.epic('客户管理'); | ||
| 68 | -// await allure.feature('客户信息'); | ||
| 69 | -// await allure.story('修改客户'); | ||
| 70 | - | ||
| 71 | -// // 先创建一个客户用于修改 | ||
| 72 | -// const originalName = generateCustomerName(); | ||
| 73 | -// const originalPhone = generatePhoneNumber(); | ||
| 74 | -// const originalIdCard = generateIdCard(); | ||
| 75 | - | ||
| 76 | -// console.log('原客户名称:', originalName); | ||
| 77 | - | ||
| 78 | -// // 步骤1:先创建客户 | ||
| 79 | -// await allure.step('创建待修改的客户', async () => { | ||
| 80 | -// await customerPage.gotoHome(); | ||
| 81 | -// await customerPage.createCustomer({ | ||
| 82 | -// name: originalName, | ||
| 83 | -// phone: originalPhone, | ||
| 84 | -// idCard: originalIdCard, | ||
| 85 | -// }); | ||
| 86 | -// }); | ||
| 87 | - | ||
| 88 | -// // 生成新的客户信息 | ||
| 89 | -// const newName = generateCustomerName(); | ||
| 90 | -// const newPhone = generatePhoneNumber(); | ||
| 91 | -// const newIdCard = generateIdCard(); | ||
| 92 | -// const randomImage = getRandomImage(); | ||
| 93 | - | ||
| 94 | -// console.log('新客户名称:', newName); | ||
| 95 | - | ||
| 96 | -// // 步骤2:执行修改客户操作 | ||
| 97 | -// await allure.step('修改客户信息', async () => { | ||
| 98 | -// await customerPage.updateCustomer( | ||
| 99 | -// originalName, | ||
| 100 | -// { | ||
| 101 | -// name: newName, | ||
| 102 | -// phone: newPhone, | ||
| 103 | -// idCard: newIdCard, | ||
| 104 | -// }, | ||
| 105 | -// { | ||
| 106 | -// creditLimit: '400', | ||
| 107 | -// licensePlate: '渝YUNI99', | ||
| 108 | -// province: '江苏省', | ||
| 109 | -// city: '连云港市', | ||
| 110 | -// district: '海州区', | ||
| 111 | -// imagePath: randomImage || undefined, | ||
| 112 | -// } | ||
| 113 | -// ); | ||
| 114 | -// await customerPage.attachScreenshot(testInfo, '修改客户成功截图'); | ||
| 115 | -// }); | ||
| 116 | - | ||
| 117 | -// // 步骤3:验证客户修改成功 | ||
| 118 | -// await allure.step('验证客户修改成功', async () => { | ||
| 119 | -// const isUpdated = await customerPage.verifyCustomerCreated(newName); | ||
| 120 | -// expect(isUpdated).toBeTruthy(); | ||
| 121 | -// }); | ||
| 122 | -// }); | ||
| 123 | - | ||
| 124 | -// test('删除客户', async ({ customerPage }, testInfo) => { | ||
| 125 | -// // 添加allure元素 | ||
| 126 | -// await allure.epic('客户管理'); | ||
| 127 | -// await allure.feature('客户信息'); | ||
| 128 | -// await allure.story('删除客户'); | ||
| 129 | - | ||
| 130 | -// // 先创建一个客户用于删除 | ||
| 131 | -// const customerName = generateCustomerName(); | ||
| 132 | -// const phone = generatePhoneNumber(); | ||
| 133 | -// const idCard = generateIdCard(); | ||
| 134 | - | ||
| 135 | -// console.log('待删除客户名称:', customerName); | ||
| 136 | - | ||
| 137 | -// // 步骤1:先创建客户 | ||
| 138 | -// await allure.step('创建待删除的客户', async () => { | ||
| 139 | -// await customerPage.gotoHome(); | ||
| 140 | -// await customerPage.createCustomer({ | ||
| 141 | -// name: customerName, | ||
| 142 | -// phone: phone, | ||
| 143 | -// idCard: idCard, | ||
| 144 | -// }); | ||
| 145 | -// }); | ||
| 146 | - | ||
| 147 | -// // 步骤2:执行删除客户操作 | ||
| 148 | -// await allure.step('删除客户', async () => { | ||
| 149 | -// await customerPage.deleteCustomer(customerName); | ||
| 150 | -// await customerPage.attachScreenshot(testInfo, '删除客户成功截图'); | ||
| 151 | -// }); | ||
| 152 | - | ||
| 153 | -// // 步骤3:验证客户删除成功 | ||
| 154 | -// await allure.step('验证客户删除成功', async () => { | ||
| 155 | -// const isDeleted = await customerPage.verifyCustomerDeleted(customerName); | ||
| 156 | -// expect(isDeleted).toBeTruthy(); | ||
| 157 | -// }); | ||
| 158 | -// }); | ||
| 159 | - | ||
| 160 | -// test('录入欠款', async ({ customerPage }, testInfo) => { | ||
| 161 | -// // 添加allure元素 | ||
| 162 | -// await allure.epic('客户管理'); | ||
| 163 | -// await allure.feature('客户欠款'); | ||
| 164 | -// await allure.story('录入欠款'); | ||
| 165 | - | ||
| 166 | -// // 先创建一个客户用于录入欠款 | ||
| 167 | -// const customerName = generateCustomerName(); | ||
| 168 | -// const phone = generatePhoneNumber(); | ||
| 169 | -// const idCard = generateIdCard(); | ||
| 170 | - | ||
| 171 | -// console.log('待录入欠款的客户名称:', customerName); | ||
| 172 | - | ||
| 173 | -// // 步骤1:先创建客户 | ||
| 174 | -// await allure.step('创建客户', async () => { | ||
| 175 | -// await customerPage.gotoHome(); | ||
| 176 | -// await customerPage.createCustomer({ | ||
| 177 | -// name: customerName, | ||
| 178 | -// phone: phone, | ||
| 179 | -// idCard: idCard, | ||
| 180 | -// }); | ||
| 181 | -// }); | ||
| 182 | - | ||
| 183 | -// // 生成欠款金额和备注 | ||
| 184 | -// const debtAmount = generateAmount(100, 9999); | ||
| 185 | -// const remark = generateRemark(50); | ||
| 186 | -// const randomImage = getRandomImage(); | ||
| 187 | - | ||
| 188 | -// console.log('欠款金额:', debtAmount); | ||
| 189 | -// console.log('备注:', remark); | ||
| 190 | - | ||
| 191 | -// // 步骤2:执行录入欠款操作 | ||
| 192 | -// await allure.step('录入欠款', async () => { | ||
| 193 | -// await customerPage.recordDebt(customerName, debtAmount, { | ||
| 194 | -// remark: remark, | ||
| 195 | -// imagePath: randomImage || undefined, | ||
| 196 | -// }); | ||
| 197 | -// await customerPage.attachScreenshot(testInfo, '录入欠款成功截图'); | ||
| 198 | -// }); | ||
| 199 | - | ||
| 200 | -// // 步骤3:验证欠款录入成功 | ||
| 201 | -// await allure.step('验证欠款录入成功', async () => { | ||
| 202 | -// const isRecorded = await customerPage.verifyDebtRecorded(debtAmount); | ||
| 203 | -// expect(isRecorded).toBeTruthy(); | ||
| 204 | -// }); | ||
| 205 | -// }); | 68 | + test('修改客户', async ({ customerPage }, testInfo) => { |
| 69 | + // 添加allure元素 | ||
| 70 | + await allure.epic('客户管理'); | ||
| 71 | + await allure.feature('客户信息'); | ||
| 72 | + await allure.story('修改客户'); | ||
| 73 | + | ||
| 74 | + // 先创建一个客户用于修改 | ||
| 75 | + const originalName = generateCustomerName(); | ||
| 76 | + const originalPhone = generatePhoneNumber(); | ||
| 77 | + const originalIdCard = generateIdCard(); | ||
| 78 | + | ||
| 79 | + // console.log('原客户名称:', originalName); | ||
| 80 | + | ||
| 81 | + // 步骤1:先创建客户 | ||
| 82 | + await allure.step('创建待修改的客户', async () => { | ||
| 83 | + await customerPage.gotoHome(); | ||
| 84 | + await customerPage.createCustomer({ | ||
| 85 | + name: originalName, | ||
| 86 | + phone: originalPhone, | ||
| 87 | + idCard: originalIdCard, | ||
| 88 | + }); | ||
| 89 | + }); | ||
| 90 | + | ||
| 91 | + // 生成新的客户信息 | ||
| 92 | + const newName = generateCustomerName(); | ||
| 93 | + const newPhone = generatePhoneNumber(); | ||
| 94 | + const newIdCard = generateIdCard(); | ||
| 95 | + const randomImage = getRandomImage(); | ||
| 96 | + | ||
| 97 | + // console.log('新客户名称:', newName); | ||
| 98 | + | ||
| 99 | + // 步骤2:执行修改客户操作 | ||
| 100 | + await allure.step('修改客户信息', async () => { | ||
| 101 | + await customerPage.updateCustomer( | ||
| 102 | + originalName, | ||
| 103 | + { | ||
| 104 | + name: newName, | ||
| 105 | + phone: newPhone, | ||
| 106 | + idCard: newIdCard, | ||
| 107 | + }, | ||
| 108 | + { | ||
| 109 | + creditLimit: '400', | ||
| 110 | + licensePlate: '渝YUNI99', | ||
| 111 | + province: '江苏省', | ||
| 112 | + city: '连云港市', | ||
| 113 | + district: '海州区', | ||
| 114 | + imagePath: randomImage || undefined, | ||
| 115 | + } | ||
| 116 | + ); | ||
| 117 | + await customerPage.attachScreenshot(testInfo, '修改客户成功截图'); | ||
| 118 | + }); | ||
| 119 | + | ||
| 120 | + // 步骤3:验证客户修改成功 | ||
| 121 | + await allure.step('验证客户修改成功', async () => { | ||
| 122 | + const isUpdated = await customerPage.verifyCustomerCreated(newName); | ||
| 123 | + expect(isUpdated).toBeTruthy(); | ||
| 124 | + }); | ||
| 125 | + }); | ||
| 126 | + | ||
| 127 | + test('删除客户', async ({ customerPage }, testInfo) => { | ||
| 128 | + // 添加allure元素 | ||
| 129 | + await allure.epic('客户管理'); | ||
| 130 | + await allure.feature('客户信息'); | ||
| 131 | + await allure.story('删除客户'); | ||
| 132 | + | ||
| 133 | + // 先创建一个客户用于删除 | ||
| 134 | + const customerName = generateCustomerName(); | ||
| 135 | + const phone = generatePhoneNumber(); | ||
| 136 | + const idCard = generateIdCard(); | ||
| 137 | + | ||
| 138 | + // console.log('待删除客户名称:', customerName); | ||
| 139 | + | ||
| 140 | + // 步骤1:先创建客户 | ||
| 141 | + await allure.step('创建待删除的客户', async () => { | ||
| 142 | + await customerPage.gotoHome(); | ||
| 143 | + await customerPage.createCustomer({ | ||
| 144 | + name: customerName, | ||
| 145 | + phone: phone, | ||
| 146 | + idCard: idCard, | ||
| 147 | + }); | ||
| 148 | + }); | ||
| 149 | + | ||
| 150 | + // 步骤2:执行删除客户操作 | ||
| 151 | + await allure.step('删除客户', async () => { | ||
| 152 | + await customerPage.deleteCustomer(customerName); | ||
| 153 | + await customerPage.attachScreenshot(testInfo, '删除客户成功截图'); | ||
| 154 | + }); | ||
| 155 | + | ||
| 156 | + // 步骤3:验证客户删除成功 | ||
| 157 | + await allure.step('验证客户删除成功', async () => { | ||
| 158 | + const isDeleted = await customerPage.verifyCustomerDeleted(customerName); | ||
| 159 | + expect(isDeleted).toBeTruthy(); | ||
| 160 | + }); | ||
| 161 | + }); | ||
| 162 | + | ||
| 163 | + test('录入欠款', async ({ customerPage }, testInfo) => { | ||
| 164 | + // 添加allure元素 | ||
| 165 | + await allure.epic('客户管理'); | ||
| 166 | + await allure.feature('客户欠款'); | ||
| 167 | + await allure.story('录入欠款'); | ||
| 168 | + | ||
| 169 | + // 先创建一个客户用于录入欠款 | ||
| 170 | + const customerName = generateCustomerName(); | ||
| 171 | + const phone = generatePhoneNumber(); | ||
| 172 | + const idCard = generateIdCard(); | ||
| 173 | + | ||
| 174 | + console.log('待录入欠款的客户名称:', customerName); | ||
| 175 | + | ||
| 176 | + // 步骤1:先创建客户 | ||
| 177 | + await allure.step('创建客户', async () => { | ||
| 178 | + await customerPage.gotoHome(); | ||
| 179 | + await customerPage.createCustomer({ | ||
| 180 | + name: customerName, | ||
| 181 | + phone: phone, | ||
| 182 | + idCard: idCard, | ||
| 183 | + }); | ||
| 184 | + }); | ||
| 185 | + | ||
| 186 | + // 生成欠款金额和备注 | ||
| 187 | + const debtAmount = generateAmount(100, 9999); | ||
| 188 | + const remark = generateRemark(50); | ||
| 189 | + const randomImage = getRandomImage(); | ||
| 190 | + | ||
| 191 | + console.log('欠款金额:', debtAmount); | ||
| 192 | + console.log('备注:', remark); | ||
| 193 | + | ||
| 194 | + // 步骤2:执行录入欠款操作 | ||
| 195 | + await allure.step('录入欠款', async () => { | ||
| 196 | + await customerPage.recordDebt(customerName, debtAmount, { | ||
| 197 | + remark: remark, | ||
| 198 | + imagePath: randomImage || undefined, | ||
| 199 | + }); | ||
| 200 | + await customerPage.attachScreenshot(testInfo, '录入欠款成功截图'); | ||
| 201 | + }); | ||
| 202 | + | ||
| 203 | + // 步骤3:验证欠款录入成功 | ||
| 204 | + await allure.step('验证欠款录入成功', async () => { | ||
| 205 | + const isRecorded = await customerPage.verifyDebtRecorded(debtAmount); | ||
| 206 | + expect(isRecorded).toBeTruthy(); | ||
| 207 | + }); | ||
| 208 | + }); | ||
| 209 | + | ||
| 210 | + test('新增客户分组', async ({ customerPage }, testInfo) => { | ||
| 211 | + // 添加allure元素 | ||
| 212 | + await allure.epic('客户管理'); | ||
| 213 | + await allure.feature('客户分组'); | ||
| 214 | + await allure.story('新增客户分组'); | ||
| 215 | + | ||
| 216 | + // 生成随机分组名称 | ||
| 217 | + const groupName = `客户分组${Date.now().toString().slice(-6)}`; | ||
| 218 | + const p = customerPage.page; | ||
| 219 | + | ||
| 220 | + // 步骤1:进入客户管理页面并点击新增分组 | ||
| 221 | + await allure.step('进入客户管理页面并点击新增分组', async () => { | ||
| 222 | + await customerPage.gotoHome(); | ||
| 223 | + await customerPage.customerMenu.click({ force: true }); | ||
| 224 | + await p.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 225 | + await p.getByText('新增分组').click(); | ||
| 226 | + }); | ||
| 227 | + | ||
| 228 | + // 步骤2:填写分组信息 | ||
| 229 | + await allure.step('填写分组信息', async () => { | ||
| 230 | + // 填写分组名称 | ||
| 231 | + await p.locator('uni-view').filter({ hasText: /^分组名称\*$/ }).first().click(); | ||
| 232 | + await p.getByRole('textbox').nth(1).fill(groupName); | ||
| 233 | + | ||
| 234 | + // 选择颜色/图标(点击第二个选项) | ||
| 235 | + await p.locator('.uni-scroll-view-content > uni-view > uni-view:nth-child(2) > .nut-cell__title > uni-view').click(); | ||
| 236 | + | ||
| 237 | + // 填写排序号 | ||
| 238 | + await p.getByRole('textbox').nth(2).click(); | ||
| 239 | + await p.getByRole('textbox').nth(2).fill('121'); | ||
| 240 | + }); | ||
| 241 | + | ||
| 242 | + // 步骤3:保存分组 | ||
| 243 | + await allure.step('保存分组', async () => { | ||
| 244 | + await p.getByText('保存').click(); | ||
| 245 | + await p.waitForTimeout(1000); | ||
| 246 | + await customerPage.attachScreenshot(testInfo, '新增客户分组成功截图'); | ||
| 247 | + }); | ||
| 248 | + | ||
| 249 | + // 步骤4:验证分组创建成功 | ||
| 250 | + await allure.step('验证分组创建成功', async () => { | ||
| 251 | + const isGroupVisible = await p.getByText('大客户').isVisible(); | ||
| 252 | + expect(isGroupVisible).toBeTruthy(); | ||
| 253 | + }); | ||
| 254 | + }); | ||
| 206 | }); | 255 | }); |
| 207 | \ No newline at end of file | 256 | \ No newline at end of file |
tests/customerGroup.spec.ts
0 → 100644
| 1 | +import { test, expect } from '@playwright/test'; | ||
| 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 | + /** | ||
| 15 | + * 生成随机分组名称(5个字以内) | ||
| 16 | + */ | ||
| 17 | + function generateGroupName(): string { | ||
| 18 | + const prefixes = ['测试', '客户', '优质', '普通', '会员', 'VIP', '大', '小', '新', '老']; | ||
| 19 | + const suffixes = ['组', '户', '客', '户', '户']; | ||
| 20 | + const prefix = prefixes[Math.floor(Math.random() * prefixes.length)]; | ||
| 21 | + const suffix = suffixes[Math.floor(Math.random() * suffixes.length)]; | ||
| 22 | + const timestamp = Date.now().toString().slice(-2); | ||
| 23 | + const name = `${prefix}${suffix}${timestamp}`; | ||
| 24 | + return name.slice(0, 5); // 确保不超过5个字 | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 生成随机排序号 | ||
| 29 | + */ | ||
| 30 | + function generateSortNumber(): string { | ||
| 31 | + return String(Math.floor(Math.random() * 999) + 1); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + test('新增客户分组', async ({ page }, testInfo) => { | ||
| 35 | + // 添加allure元素 | ||
| 36 | + await allure.epic('客户管理'); | ||
| 37 | + await allure.feature('客户分组'); | ||
| 38 | + await allure.story('新增客户分组'); | ||
| 39 | + | ||
| 40 | + // 生成随机分组名称 | ||
| 41 | + const groupName = generateGroupName(); | ||
| 42 | + console.log('新增分组名称:', groupName); | ||
| 43 | + | ||
| 44 | + // 步骤1:进入客户分组页面 | ||
| 45 | + await allure.step('进入客户分组页面', async () => { | ||
| 46 | + await page.goto('/'); | ||
| 47 | + await page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 48 | + await page.getByText('更多 >').click(); | ||
| 49 | + await page.waitForTimeout(500); | ||
| 50 | + await page.getByText('客户分组').first().click(); | ||
| 51 | + await page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 52 | + }); | ||
| 53 | + | ||
| 54 | + // 步骤2:点击新建按钮 | ||
| 55 | + await allure.step('点击新建按钮', async () => { | ||
| 56 | + await page.getByText('新建').click(); | ||
| 57 | + await page.waitForTimeout(500); | ||
| 58 | + }); | ||
| 59 | + | ||
| 60 | + // 步骤3:填写分组信息 | ||
| 61 | + await allure.step('填写分组信息', async () => { | ||
| 62 | + // 填写分组名称 | ||
| 63 | + await page.getByRole('textbox').nth(1).click(); | ||
| 64 | + await page.getByRole('textbox').nth(1).fill(groupName); | ||
| 65 | + | ||
| 66 | + // 填写备注/排序 | ||
| 67 | + await page.getByRole('textbox').nth(2).click(); | ||
| 68 | + await page.getByRole('textbox').nth(2).fill('自动化测试分组'); | ||
| 69 | + }); | ||
| 70 | + | ||
| 71 | + // 步骤4:保存分组 | ||
| 72 | + await allure.step('保存分组', async () => { | ||
| 73 | + await page.getByText('确定').click(); | ||
| 74 | + await page.waitForTimeout(1000); | ||
| 75 | + }); | ||
| 76 | + | ||
| 77 | + // 步骤5:验证分组创建成功 - 使用正则匹配检查页面是否出现新增的内容 | ||
| 78 | + await allure.step('验证分组创建成功', async () => { | ||
| 79 | + await page.waitForTimeout(1000); // 等待页面刷新 | ||
| 80 | + // 使用正则匹配分组名称 | ||
| 81 | + const isGroupVisible = await page.locator('uni-view').filter({ hasText: new RegExp(`^${groupName}$`) }).first().isVisible({ timeout: 5000 }).catch(() => false); | ||
| 82 | + expect(isGroupVisible).toBeTruthy(); | ||
| 83 | + }); | ||
| 84 | + }); | ||
| 85 | + | ||
| 86 | + test('修改客户分组', async ({ page }, testInfo) => { | ||
| 87 | + // 添加allure元素 | ||
| 88 | + await allure.epic('客户管理'); | ||
| 89 | + await allure.feature('客户分组'); | ||
| 90 | + await allure.story('修改客户分组'); | ||
| 91 | + | ||
| 92 | + // 先生成一个分组用于修改 | ||
| 93 | + const originalGroupName = generateGroupName(); | ||
| 94 | + const newGroupName = generateGroupName(); | ||
| 95 | + console.log('原分组名称:', originalGroupName); | ||
| 96 | + console.log('新分组名称:', newGroupName); | ||
| 97 | + | ||
| 98 | + // 步骤1:进入客户分组页面并创建分组 | ||
| 99 | + await allure.step('进入客户分组页面并创建分组', async () => { | ||
| 100 | + await page.goto('/'); | ||
| 101 | + await page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 102 | + await page.getByText('更多 >').click(); | ||
| 103 | + await page.waitForTimeout(500); | ||
| 104 | + await page.getByText('客户分组').first().click(); | ||
| 105 | + await page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 106 | + | ||
| 107 | + // 创建分组 | ||
| 108 | + await page.getByText('新建').click(); | ||
| 109 | + await page.waitForTimeout(500); | ||
| 110 | + // 点击分组名称输入框 | ||
| 111 | + await page.locator('uni-view').filter({ hasText: /^分组名称\*$/ }).first().click(); | ||
| 112 | + await page.getByRole('textbox').nth(1).fill(originalGroupName); | ||
| 113 | + await page.getByText('确定').click(); | ||
| 114 | + await page.waitForTimeout(1000); | ||
| 115 | + }); | ||
| 116 | + | ||
| 117 | + // 步骤2:搜索并修改分组 | ||
| 118 | + await allure.step('搜索并修改分组', async () => { | ||
| 119 | + // 在搜索框中输入原分组名称 | ||
| 120 | + await page.getByRole('textbox').click(); | ||
| 121 | + await page.getByRole('textbox').fill(originalGroupName); | ||
| 122 | + | ||
| 123 | + // 点击搜索结果中的分组 | ||
| 124 | + await page.locator('uni-view').filter({ hasText: new RegExp(`^${originalGroupName}$`) }).first().click(); | ||
| 125 | + await page.waitForTimeout(500); | ||
| 126 | + | ||
| 127 | + // 点击编辑按钮 | ||
| 128 | + await page.getByText('编辑').click(); | ||
| 129 | + await page.waitForTimeout(500); | ||
| 130 | + | ||
| 131 | + // 清空分组名称并填写新名称 | ||
| 132 | + await page.getByRole('textbox').nth(1).click(); | ||
| 133 | + await page.locator('.nut-input__clear-icon').first().click(); | ||
| 134 | + await page.getByRole('textbox').nth(1).click(); | ||
| 135 | + await page.getByRole('textbox').nth(1).fill(newGroupName); | ||
| 136 | + | ||
| 137 | + // 直接填写新排序号(新增时未填写,无需清除) | ||
| 138 | + const newSortNumber = generateSortNumber(); | ||
| 139 | + await page.getByRole('textbox').nth(2).click(); | ||
| 140 | + await page.getByRole('textbox').nth(2).fill(newSortNumber); | ||
| 141 | + | ||
| 142 | + await page.getByText('确定').click(); | ||
| 143 | + await page.waitForTimeout(1000); | ||
| 144 | + }); | ||
| 145 | + | ||
| 146 | + // 步骤3:验证分组修改成功 - 使用搜索验证修改后的内容 | ||
| 147 | + await allure.step('验证分组修改成功', async () => { | ||
| 148 | + // 在搜索框中输入新分组名称进行验证 | ||
| 149 | + await page.waitForTimeout(500); | ||
| 150 | + await page.getByRole('textbox').click(); | ||
| 151 | + await page.getByRole('textbox').fill(newGroupName); | ||
| 152 | + await page.waitForTimeout(500); | ||
| 153 | + | ||
| 154 | + const isNewGroupVisible = await page.locator('uni-view').filter({ hasText: new RegExp(`^${newGroupName}$`) }).first().isVisible({ timeout: 5000 }).catch(() => false); | ||
| 155 | + expect(isNewGroupVisible).toBeTruthy(); | ||
| 156 | + }); | ||
| 157 | + }); | ||
| 158 | + | ||
| 159 | + test('删除客户分组', async ({ page }, testInfo) => { | ||
| 160 | + // 添加allure元素 | ||
| 161 | + await allure.epic('客户管理'); | ||
| 162 | + await allure.feature('客户分组'); | ||
| 163 | + await allure.story('删除客户分组'); | ||
| 164 | + | ||
| 165 | + // 先生成一个分组用于删除 | ||
| 166 | + const groupName = generateGroupName(); | ||
| 167 | + console.log('待删除分组名称:', groupName); | ||
| 168 | + | ||
| 169 | + // 步骤1:进入客户分组页面并创建分组 | ||
| 170 | + await allure.step('进入客户分组页面并创建分组', async () => { | ||
| 171 | + await page.goto('/'); | ||
| 172 | + await page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 173 | + await page.getByText('更多 >').click(); | ||
| 174 | + await page.waitForTimeout(500); | ||
| 175 | + await page.getByText('客户分组').first().click(); | ||
| 176 | + await page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 177 | + | ||
| 178 | + // 创建分组 | ||
| 179 | + await page.getByText('新建').click(); | ||
| 180 | + await page.waitForTimeout(500); | ||
| 181 | + await page.getByRole('textbox').nth(1).click(); | ||
| 182 | + await page.getByRole('textbox').nth(1).fill(groupName); | ||
| 183 | + await page.getByText('确定').click(); | ||
| 184 | + await page.waitForTimeout(1000); | ||
| 185 | + }); | ||
| 186 | + | ||
| 187 | + // 步骤2:搜索并删除分组 | ||
| 188 | + await allure.step('搜索并删除分组', async () => { | ||
| 189 | + // 在搜索框中输入分组名称 | ||
| 190 | + await page.getByRole('textbox').click(); | ||
| 191 | + await page.getByRole('textbox').fill(groupName); | ||
| 192 | + | ||
| 193 | + // 搜索后自动选中,直接点击删除按钮 | ||
| 194 | + await page.getByText('删除').click(); | ||
| 195 | + await page.waitForTimeout(500); | ||
| 196 | + | ||
| 197 | + // 确认删除 | ||
| 198 | + await page.getByText('确定', { exact: true }).click(); | ||
| 199 | + // await page.waitForTimeout(1000); | ||
| 200 | + | ||
| 201 | + // 点击清除按钮 | ||
| 202 | + await page.locator('.nut-searchbar__search-icon').click(); | ||
| 203 | + // 清除后等待页面刷新 | ||
| 204 | + await page.waitForTimeout(1000); | ||
| 205 | + }); | ||
| 206 | + | ||
| 207 | + // 步骤3:验证分组删除成功 | ||
| 208 | + await allure.step('验证分组删除成功', async () => { | ||
| 209 | + // 搜索已删除的分组名称 | ||
| 210 | + await page.getByRole('textbox').fill(groupName); | ||
| 211 | + await page.waitForTimeout(1000); | ||
| 212 | + | ||
| 213 | + // 验证搜索结果中是否还能找到该分组(找不到才是删除成功) | ||
| 214 | + const count = await page.locator('uni-view').filter({ hasText: new RegExp(`^${groupName}$`) }).count(); | ||
| 215 | + expect(count).toBe(0); | ||
| 216 | + }); | ||
| 217 | + }); | ||
| 218 | +}); |
tests/login.setup.ts
| @@ -33,6 +33,6 @@ setup('authenticate', async ({ page }) => { | @@ -33,6 +33,6 @@ setup('authenticate', async ({ page }) => { | ||
| 33 | // await page.waitForSelector('text=个人中心', { timeout: 60000 }); | 33 | // await page.waitForSelector('text=个人中心', { timeout: 60000 }); |
| 34 | 34 | ||
| 35 | // 5. 登录成功后,保存状态 | 35 | // 5. 登录成功后,保存状态 |
| 36 | -// await page.context().storageState({ path: authFile }); | ||
| 37 | -// console.log('认证状态已保存到', authFile); | 36 | + await page.context().storageState({ path: authFile }); |
| 37 | + console.log('认证状态已保存到', authFile); | ||
| 38 | }); | 38 | }); |
tests/product.spec.ts
| @@ -37,17 +37,5 @@ test.describe('商品管理', () => { | @@ -37,17 +37,5 @@ test.describe('商品管理', () => { | ||
| 37 | // await productPage.expectProductVisible(productName); | 37 | // await productPage.expectProductVisible(productName); |
| 38 | }); | 38 | }); |
| 39 | 39 | ||
| 40 | - // test('新增商品 - 使用分步操作', async ({ productPage }) => { | ||
| 41 | - // // 生成唯一商品名 | ||
| 42 | - // const productName = generateUniqueProductName('苹果'); | ||
| 43 | - | ||
| 44 | - // // 分步操作示例 | ||
| 45 | - // await productPage.openAddProductForm(); | ||
| 46 | - // await productPage.enterProductName(productName); | ||
| 47 | - // await productPage.selectCategory('苹果'); | ||
| 48 | - // await productPage.clickSave(); | ||
| 49 | - | ||
| 50 | - // // 验证商品创建成功 | ||
| 51 | - // await productPage.expectProductVisible(productName); | ||
| 52 | - // }); | 40 | + |
| 53 | }); | 41 | }); |
| 54 | \ No newline at end of file | 42 | \ No newline at end of file |
utils/dataGenerator.ts
| @@ -27,7 +27,11 @@ export function generateCustomerName(): string { | @@ -27,7 +27,11 @@ export function generateCustomerName(): string { | ||
| 27 | const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]; | 27 | const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]; |
| 28 | const givenName = givenNames[Math.floor(Math.random() * givenNames.length)]; | 28 | const givenName = givenNames[Math.floor(Math.random() * givenNames.length)]; |
| 29 | 29 | ||
| 30 | - return familyName + givenName; | 30 | + // 使用时间戳(后6位)+ 随机数后缀,确保不易重复,只使用数字和字母 |
| 31 | + const timestamp = Date.now().toString().slice(-6); | ||
| 32 | + // const random = Math.floor(Math.random() * 100).toString().padStart(2, '0'); | ||
| 33 | + | ||
| 34 | + return `${familyName}${givenName}${timestamp}`; | ||
| 31 | } | 35 | } |
| 32 | 36 | ||
| 33 | // 随机生成身份证号码(18位) | 37 | // 随机生成身份证号码(18位) |
| @@ -202,3 +206,62 @@ export function generateAmount(min: number = 1, max: number = 9999): string { | @@ -202,3 +206,62 @@ export function generateAmount(min: number = 1, max: number = 9999): string { | ||
| 202 | const amount = Math.floor(Math.random() * (max - min + 1)) + min; | 206 | const amount = Math.floor(Math.random() * (max - min + 1)) + min; |
| 203 | return amount.toString(); | 207 | return amount.toString(); |
| 204 | } | 208 | } |
| 209 | + | ||
| 210 | +// 随机生成车牌号 | ||
| 211 | +export function generateLicensePlate(): string { | ||
| 212 | + // 省份简称(中国各省份) | ||
| 213 | + const provinces = ['京', '津', '沪', '渝', '冀', '晋', '蒙', '辽', '吉', '黑', '苏', '浙', '皖', '闽', '赣', '鲁', '豫', '鄂', '湘', '粤', '桂', '琼', '川', '贵', '云', '藏', '陕', '甘', '青', '宁', '新']; | ||
| 214 | + | ||
| 215 | + // 字母(车牌号第二位) | ||
| 216 | + const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; | ||
| 217 | + | ||
| 218 | + // 随机选择省份 | ||
| 219 | + const province = provinces[Math.floor(Math.random() * provinces.length)]; | ||
| 220 | + | ||
| 221 | + // 随机选择字母 | ||
| 222 | + const letter = letters[Math.floor(Math.random() * letters.length)]; | ||
| 223 | + | ||
| 224 | + // 随机生成5位数字或字母组合(第三位到第七位) | ||
| 225 | + const chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; | ||
| 226 | + let suffix = ''; | ||
| 227 | + for (let i = 0; i < 5; i++) { | ||
| 228 | + suffix += chars[Math.floor(Math.random() * chars.length)]; | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + return province + letter + suffix; | ||
| 232 | +} | ||
| 233 | + | ||
| 234 | +// 随机生成省市区数据 | ||
| 235 | +export interface RegionData { | ||
| 236 | + province: string; | ||
| 237 | + city: string; | ||
| 238 | + district: string; | ||
| 239 | +} | ||
| 240 | + | ||
| 241 | +export function generateRegion(): RegionData { | ||
| 242 | + // 省市区数据(示例数据,可根据实际业务扩展) | ||
| 243 | + const regions: RegionData[] = [ | ||
| 244 | + { province: '山西省', city: '长治市', district: '潞城区' }, | ||
| 245 | + { province: '北京市', city: '北京市', district: '朝阳区' }, | ||
| 246 | + { province: '上海市', city: '上海市', district: '浦东新区' }, | ||
| 247 | + { province: '广东省', city: '广州市', district: '天河区' }, | ||
| 248 | + { province: '浙江省', city: '杭州市', district: '西湖区' }, | ||
| 249 | + { province: '江苏省', city: '南京市', district: '鼓楼区' }, | ||
| 250 | + { province: '四川省', city: '成都市', district: '武侯区' }, | ||
| 251 | + { province: '湖北省', city: '武汉市', district: '洪山区' }, | ||
| 252 | + { province: '湖南省', city: '长沙市', district: '岳麓区' }, | ||
| 253 | + { province: '河南省', city: '郑州市', district: '金水区' }, | ||
| 254 | + { province: '山东省', city: '济南市', district: '历下区' }, | ||
| 255 | + { province: '福建省', city: '福州市', district: '鼓楼区' }, | ||
| 256 | + { province: '安徽省', city: '合肥市', district: '蜀山区' }, | ||
| 257 | + { province: '江西省', city: '南昌市', district: '东湖区' }, | ||
| 258 | + { province: '陕西省', city: '西安市', district: '雁塔区' }, | ||
| 259 | + { province: '甘肃省', city: '兰州市', district: '城关区' }, | ||
| 260 | + { province: '云南省', city: '昆明市', district: '五华区' }, | ||
| 261 | + { province: '贵州省', city: '贵阳市', district: '南明区' }, | ||
| 262 | + { province: '河北省', city: '石家庄市', district: '长安区' }, | ||
| 263 | + { province: '辽宁省', city: '沈阳市', district: '和平区' }, | ||
| 264 | + ]; | ||
| 265 | + | ||
| 266 | + return regions[Math.floor(Math.random() * regions.length)]; | ||
| 267 | +} |