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 | 318 | if (uploadButtonLocator) { |
| 319 | 319 | await uploadButtonLocator.click(); |
| 320 | 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 | 354 | // 等待上传完成 |
| 341 | 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 | 94 | this.confirmRegionButton = page.getByText('确认选择'); |
| 95 | 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 | 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 | 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 | 228 | * @param amount 赊欠额度金额 |
| 208 | 229 | */ |
| ... | ... | @@ -229,24 +250,12 @@ export class CustomerPage extends BasePage { |
| 229 | 250 | * @param city 城市(如:长治市) |
| 230 | 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 | 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 | 330 | |
| 322 | 331 | // 如果有省市区配置 |
| 323 | 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 | 356 | */ |
| 348 | 357 | async verifyCustomerCreated(customerName: string): Promise<boolean> { |
| 349 | 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 | 374 | await this.page.waitForSelector(selectors.customerInList(customerName), { timeout: 10000 }); |
| 351 | 375 | return true; |
| 352 | 376 | } catch { |
| ... | ... | @@ -364,7 +388,12 @@ export class CustomerPage extends BasePage { |
| 364 | 388 | // 点击搜索框区域(使用 force: true 绕过遮罩层) |
| 365 | 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 | 397 | await this.page.waitForTimeout(1000); |
| 369 | 398 | } |
| 370 | 399 | |
| ... | ... | @@ -373,7 +402,10 @@ export class CustomerPage extends BasePage { |
| 373 | 402 | * @param customerName 客户名称 |
| 374 | 403 | */ |
| 375 | 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 | 409 | await this.page.waitForLoadState('networkidle', { timeout: 30000 }); |
| 378 | 410 | } |
| 379 | 411 | |
| ... | ... | @@ -438,9 +470,10 @@ export class CustomerPage extends BasePage { |
| 438 | 470 | await this.clearAndFillIdCard(customerInfo.idCard); |
| 439 | 471 | |
| 440 | 472 | if (options?.creditLimit) { |
| 441 | - // 清除并填写赊欠额度(使用 force: true 绕过遮罩层) | |
| 473 | + // 先确保赊欠额度开关已开启 | |
| 474 | + await this.ensureCreditLimitEnabled(); | |
| 475 | + // 直接填写赊欠额度(fill会自动清除) | |
| 442 | 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 | 477 | await this.creditLimitInput.fill(options.creditLimit); |
| 445 | 478 | } |
| 446 | 479 | |
| ... | ... | @@ -449,7 +482,7 @@ export class CustomerPage extends BasePage { |
| 449 | 482 | } |
| 450 | 483 | |
| 451 | 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 | 488 | if (customerInfo.detailedAddress) { |
| ... | ... | @@ -476,6 +509,9 @@ export class CustomerPage extends BasePage { |
| 476 | 509 | await this.clickCustomerItem(customerName); |
| 477 | 510 | await this.clickDeleteButton(); |
| 478 | 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 | 521 | async verifyCustomerDeleted(customerName: string): Promise<boolean> { |
| 486 | 522 | await this.gotoHome(); |
| 487 | 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 | 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 | 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 | 548 | * @param customerName 客户名称 |
| 542 | 549 | * @param amount 欠款金额 |
| 543 | 550 | * @param options 可选配置 |
| ... | ... | @@ -546,12 +553,12 @@ export class CustomerPage extends BasePage { |
| 546 | 553 | customerName: string, |
| 547 | 554 | amount: string, |
| 548 | 555 | options?: { |
| 549 | - typeIndex?: number; // 欠款类型索引 | |
| 556 | + debtType?: string; // 欠款类型(如 '15') | |
| 550 | 557 | remark?: string; // 备注 |
| 551 | 558 | imagePath?: string; // 图片路径 |
| 552 | 559 | } |
| 553 | 560 | ): Promise<void> { |
| 554 | - // 先返回首页确保页面状态干净(避免遮罩层阻挡) | |
| 561 | + // 先返回首页确保页面状态干净 | |
| 555 | 562 | await this.gotoHome(); |
| 556 | 563 | // 打开客户管理并搜索 |
| 557 | 564 | await this.openCustomerManagement(); |
| ... | ... | @@ -559,39 +566,79 @@ export class CustomerPage extends BasePage { |
| 559 | 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 | 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 | 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 | 642 | return false; |
| 596 | 643 | } |
| 597 | 644 | } | ... | ... |
scripts/save-auth.ts
| 1 | 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 | 8 | const TEST_PHONE = process.env.phone; |
| 6 | 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 | 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 | 9 | test.describe('客户管理', () => { |
| 10 | 10 | // 使用已保存的认证状态 |
| 11 | 11 | test.use({ storageState: 'auth.json' }); |
| 12 | + | |
| 13 | + // 强制测试串行执行,避免并行测试之间的干扰 | |
| 14 | + test.describe.configure({ mode: 'serial' }); | |
| 12 | 15 | |
| 13 | 16 | test('新增客户', async ({ customerPage }, testInfo) => { |
| 14 | 17 | // 添加allure元素 |
| ... | ... | @@ -24,7 +27,7 @@ test.describe('客户管理', () => { |
| 24 | 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 | 32 | return { name, phone, idCard, detailedAddress }; |
| 30 | 33 | }); |
| ... | ... | @@ -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 | 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 | 33 | // await page.waitForSelector('text=个人中心', { timeout: 60000 }); |
| 34 | 34 | |
| 35 | 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 | 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 | 42 | \ No newline at end of file | ... | ... |
utils/dataGenerator.ts
| ... | ... | @@ -27,7 +27,11 @@ export function generateCustomerName(): string { |
| 27 | 27 | const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]; |
| 28 | 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 | 37 | // 随机生成身份证号码(18位) |
| ... | ... | @@ -202,3 +206,62 @@ export function generateAmount(min: number = 1, max: number = 9999): string { |
| 202 | 206 | const amount = Math.floor(Math.random() * (max - min + 1)) + min; |
| 203 | 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 | +} | ... | ... |