Commit 33bc2f2f88ab7800fbf3b853292968d1cd4b6f9c

Authored by 赵旭婷
1 parent 30d1471a

去掉硬编码

.gitignore
@@ -10,4 +10,5 @@ node_modules/ @@ -10,4 +10,5 @@ node_modules/
10 /.env 10 /.env
11 /allure-results/ 11 /allure-results/
12 /plans/ 12 /plans/
13 -/reports/  
14 \ No newline at end of file 13 \ No newline at end of file
  14 +/reports/
  15 +/screenshots/
15 \ No newline at end of file 16 \ No newline at end of file
Jenkinsfile 0 → 100644
  1 +// Jenkinsfile - UI自动化测试流水线配置
  2 +// 使用方法:在 Jenkins 中创建 Pipeline 任务,选择 "Pipeline script from SCM" 或直接粘贴此文件内容
  3 +
  4 +pipeline {
  5 + agent any
  6 +
  7 + tools {
  8 + nodejs 'NodeJS-18' // Jenkins 中配置的 Node.js 工具名称
  9 + allure 'Allure' // Jenkins 中配置的 Allure 工具名称
  10 + }
  11 +
  12 + environment {
  13 + // 环境变量配置
  14 + NODE_ENV = 'test'
  15 + // 测试环境 URL(可在 Jenkins 中覆盖)
  16 + BASE_URL = credentials('test-base-url') ?: 'https://erp-pad.test.gszdtop.com'
  17 + }
  18 +
  19 + parameters {
  20 + choice(
  21 + name: 'TEST_SUITE',
  22 + choices: ['all', 'customer', 'product', 'sale', 'consignment'],
  23 + description: '选择要执行的测试套件'
  24 + )
  25 + choice(
  26 + name: 'BROWSER',
  27 + choices: ['chromium', 'firefox', 'webkit'],
  28 + description: '选择浏览器'
  29 + )
  30 + booleanParam(
  31 + name: 'HEADLESS',
  32 + defaultValue: true,
  33 + description: '是否使用无头模式'
  34 + )
  35 + string(
  36 + name: 'GREP',
  37 + defaultValue: '',
  38 + description: '测试用例过滤(留空执行全部)'
  39 + )
  40 + }
  41 +
  42 + stages {
  43 + stage('Checkout') {
  44 + steps {
  45 + echo '📥 拉取代码...'
  46 + git branch: 'main', url: 'https://your-git-repo-url/xfbhAutoTest.git'
  47 + }
  48 + }
  49 +
  50 + stage('Install Dependencies') {
  51 + steps {
  52 + echo '📦 安装依赖...'
  53 + sh 'npm install'
  54 + }
  55 + }
  56 +
  57 + stage('Install Playwright Browsers') {
  58 + steps {
  59 + echo '🌐 安装浏览器...'
  60 + sh "npx playwright install ${params.BROWSER}"
  61 + sh "npx playwright install-deps ${params.BROWSER}"
  62 + }
  63 + }
  64 +
  65 + stage('Run Tests') {
  66 + steps {
  67 + echo '🧪 执行测试...'
  68 + script {
  69 + def testCommand = "npx playwright test"
  70 +
  71 + // 选择测试套件
  72 + if (params.TEST_SUITE != 'all') {
  73 + testCommand += " tests/${params.TEST_SUITE}.spec.ts"
  74 + }
  75 +
  76 + // 选择浏览器
  77 + testCommand += " --project=${params.BROWSER}"
  78 +
  79 + // 无头模式
  80 + if (!params.HEADLESS) {
  81 + testCommand += " --headed"
  82 + }
  83 +
  84 + // 过滤测试用例
  85 + if (params.GREP) {
  86 + testCommand += " -g \"${params.GREP}\""
  87 + }
  88 +
  89 + sh testCommand
  90 + }
  91 + }
  92 + }
  93 + }
  94 +
  95 + post {
  96 + always {
  97 + echo '📊 生成测试报告...'
  98 +
  99 + // 发布 Allure 报告
  100 + allure includeProperties: false,
  101 + jdk: '',
  102 + results: [[path: 'allure-results']]
  103 +
  104 + // 归档 Playwright HTML 报告
  105 + publishHTML(target: [
  106 + allowMissing: false,
  107 + alwaysLinkToLastBuild: false,
  108 + keepAll: true,
  109 + reportDir: 'playwright-report',
  110 + reportFiles: 'index.html',
  111 + reportName: 'Playwright Report'
  112 + ])
  113 +
  114 + // 归档测试结果
  115 + archiveArtifacts artifacts: 'playwright-report/**, test-results/**',
  116 + allowEmptyArchive: true
  117 + }
  118 +
  119 + success {
  120 + echo '✅ 测试执行成功!'
  121 + }
  122 +
  123 + failure {
  124 + echo '❌ 测试执行失败,请检查测试报告!'
  125 + }
  126 +
  127 + unstable {
  128 + echo '⚠️ 测试执行完成,但存在失败的用例!'
  129 + }
  130 + }
  131 +}
  132 +
  133 +// ============ 定时执行配置 ============
  134 +// 如需定时执行,在 pipeline 块内添加以下 triggers 配置:
  135 +//
  136 +// triggers {
  137 +// // 每天 6:00 执行(北京时间)
  138 +// cron('0 22 * * *') // UTC 22:00 = 北京时间 06:00
  139 +//
  140 +// // 或者使用轮询 SCM(代码变更时触发)
  141 +// pollSCM('H/5 * * * *') // 每5分钟检查一次代码变更
  142 +// }
  143 +
  144 +// ============ 邮件通知配置 ============
  145 +// 如需邮件通知,在 post 块中添加:
  146 +//
  147 +// emailext(
  148 +// subject: "UI自动化测试报告 - ${env.JOB_NAME} #${env.BUILD_NUMBER}",
  149 +// body: """
  150 +// <h2>测试执行完成</h2>
  151 +// <p>构建状态: ${currentBuild.currentResult}</p>
  152 +// <p>构建链接: ${env.BUILD_URL}</p>
  153 +// <p>Allure报告: ${env.BUILD_URL}allure/</p>
  154 +// <p>HTML报告: ${env.BUILD_URL}Playwright_Report/</p>
  155 +// """,
  156 +// to: 'team@example.com',
  157 +// from: 'jenkins@example.com'
  158 +// )
0 \ No newline at end of file 159 \ No newline at end of file
pages/basePage.ts
@@ -32,7 +32,10 @@ export abstract class BasePage { @@ -32,7 +32,10 @@ export abstract class BasePage {
32 * @param path 相对路径 32 * @param path 相对路径
33 */ 33 */
34 async navigate(path: string = '/'): Promise<void> { 34 async navigate(path: string = '/'): Promise<void> {
35 - const baseURL = process.env.BASE_URL || 'https://erp-pad.test.gszdtop.com'; 35 + const baseURL = process.env.BASE_URL;
  36 + if (!baseURL) {
  37 + throw new Error('BASE_URL 环境变量未设置,请设置 BASE_URL 后再运行');
  38 + }
36 await this.page.goto(`${baseURL}/#${path}`); 39 await this.page.goto(`${baseURL}/#${path}`);
37 } 40 }
38 41
@@ -172,7 +175,17 @@ export abstract class BasePage { @@ -172,7 +175,17 @@ export abstract class BasePage {
172 * @param name 截图名称 175 * @param name 截图名称
173 */ 176 */
174 async takeScreenshot(name: string): Promise<Buffer> { 177 async takeScreenshot(name: string): Promise<Buffer> {
175 - return this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true }); 178 + // 使用项目根目录下的 screenshots 文件夹
  179 + const fs = require('fs');
  180 + const path = require('path');
  181 + const screenshotsDir = path.join(process.cwd(), 'screenshots');
  182 +
  183 + // 确保 screenshots 目录存在
  184 + if (!fs.existsSync(screenshotsDir)) {
  185 + fs.mkdirSync(screenshotsDir, { recursive: true });
  186 + }
  187 +
  188 + return this.page.screenshot({ path: path.join(screenshotsDir, `${name}.png`), fullPage: true });
176 } 189 }
177 190
178 /** 191 /**
pages/customerPage.ts
@@ -80,7 +80,7 @@ export class CustomerPage extends BasePage { @@ -80,7 +80,7 @@ export class CustomerPage extends BasePage {
80 constructor(page: Page) { 80 constructor(page: Page) {
81 super(page); 81 super(page);
82 82
83 - this.customerMenu = page.getByText('客户管理'); 83 + this.customerMenu = page.getByText('客户管理').first();
84 this.addCustomerButton = page.getByText('新增客户'); 84 this.addCustomerButton = page.getByText('新增客户');
85 this.customerNameInput = page.getByRole('textbox').nth(1); 85 this.customerNameInput = page.getByRole('textbox').nth(1);
86 this.phoneInput = page.getByRole('spinbutton').first(); 86 this.phoneInput = page.getByRole('spinbutton').first();
@@ -109,7 +109,7 @@ export class CustomerPage extends BasePage { @@ -109,7 +109,7 @@ export class CustomerPage extends BasePage {
109 * 打开客户管理页面 109 * 打开客户管理页面
110 */ 110 */
111 async openCustomerManagement(): Promise<void> { 111 async openCustomerManagement(): Promise<void> {
112 - await this.customerMenu.click(); 112 + await this.customerMenu.click({ force: true });
113 await this.page.waitForLoadState('networkidle', { timeout: 30000 }); 113 await this.page.waitForLoadState('networkidle', { timeout: 30000 });
114 } 114 }
115 115
@@ -131,6 +131,17 @@ export class CustomerPage extends BasePage { @@ -131,6 +131,17 @@ export class CustomerPage extends BasePage {
131 } 131 }
132 132
133 /** 133 /**
  134 + * 清除并填写客户名称
  135 + * @param name 客户名称
  136 + */
  137 + async clearAndFillCustomerName(name: string): Promise<void> {
  138 + await this.customerNameInput.click();
  139 + // 点击清除图标
  140 + await this.page.locator('.nut-input__clear-icon').first().click();
  141 + await this.customerNameInput.fill(name);
  142 + }
  143 +
  144 + /**
134 * 填写手机号 145 * 填写手机号
135 * @param phone 手机号 146 * @param phone 手机号
136 */ 147 */
@@ -140,6 +151,17 @@ export class CustomerPage extends BasePage { @@ -140,6 +151,17 @@ export class CustomerPage extends BasePage {
140 } 151 }
141 152
142 /** 153 /**
  154 + * 清除并填写手机号
  155 + * @param phone 手机号
  156 + */
  157 + async clearAndFillPhone(phone: string): Promise<void> {
  158 + await this.phoneInput.click();
  159 + // 点击手机号清除图标
  160 + await this.page.locator('uni-view:nth-child(3) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__clear > .nut-input__clear-icon').click();
  161 + await this.phoneInput.fill(phone);
  162 + }
  163 +
  164 + /**
143 * 填写身份证号 165 * 填写身份证号
144 * @param idCard 身份证号 166 * @param idCard 身份证号
145 */ 167 */
@@ -149,6 +171,17 @@ export class CustomerPage extends BasePage { @@ -149,6 +171,17 @@ export class CustomerPage extends BasePage {
149 } 171 }
150 172
151 /** 173 /**
  174 + * 清除并填写身份证号
  175 + * @param idCard 身份证号
  176 + */
  177 + async clearAndFillIdCard(idCard: string): Promise<void> {
  178 + await this.idCardInput.click();
  179 + // 点击身份证清除图标
  180 + await this.page.locator('uni-view:nth-child(4) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__clear > .nut-input__clear-icon').click();
  181 + await this.idCardInput.fill(idCard);
  182 + }
  183 +
  184 + /**
152 * 选择客户分组(选择"普通客户") 185 * 选择客户分组(选择"普通客户")
153 */ 186 */
154 async selectCustomerGroup(): Promise<void> { 187 async selectCustomerGroup(): Promise<void> {
@@ -320,4 +353,246 @@ export class CustomerPage extends BasePage { @@ -320,4 +353,246 @@ export class CustomerPage extends BasePage {
320 return false; 353 return false;
321 } 354 }
322 } 355 }
323 -}  
324 \ No newline at end of file 356 \ No newline at end of file
  357 +
  358 + // ==================== 修改/删除客户相关方法 ====================
  359 +
  360 + /**
  361 + * 搜索客户
  362 + * @param customerName 客户名称
  363 + */
  364 + async searchCustomer(customerName: string): Promise<void> {
  365 + // 点击搜索框区域(使用 force: true 绕过遮罩层)
  366 + await this.page.locator('uni-view').filter({ hasText: /^客户名称\/手机号$/ }).nth(2).click({ force: true });
  367 + // 在输入框中填写客户名称
  368 + await this.page.getByRole('textbox').fill(customerName);
  369 + await this.page.waitForTimeout(1000);
  370 + }
  371 +
  372 + /**
  373 + * 点击客户进入详情页面
  374 + * @param customerName 客户名称
  375 + */
  376 + async clickCustomerItem(customerName: string): Promise<void> {
  377 + await this.page.getByText(customerName).first().click();
  378 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  379 + }
  380 +
  381 + /**
  382 + * 点击编辑按钮
  383 + */
  384 + async clickEditButton(): Promise<void> {
  385 + await this.page.getByText('修改客户').click();
  386 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  387 + }
  388 +
  389 + /**
  390 + * 点击删除按钮
  391 + */
  392 + async clickDeleteButton(): Promise<void> {
  393 + await this.page.getByText('删除客户').click();
  394 + await this.page.waitForTimeout(500);
  395 + }
  396 +
  397 + /**
  398 + * 确认删除
  399 + */
  400 + async confirmDelete(): Promise<void> {
  401 + await this.page.getByText('确定', { exact: true }).click();
  402 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  403 + }
  404 +
  405 + /**
  406 + * 修改客户 - 复用新增方法
  407 + * @param originalName 原客户名称(用于搜索)
  408 + * @param customerInfo 新的客户信息
  409 + * @param options 可选配置
  410 + */
  411 + async updateCustomer(
  412 + originalName: string,
  413 + customerInfo: {
  414 + name: string;
  415 + phone: string;
  416 + idCard: string;
  417 + detailedAddress?: string;
  418 + },
  419 + options?: {
  420 + creditLimit?: string;
  421 + licensePlate?: string;
  422 + province?: string;
  423 + city?: string;
  424 + district?: string;
  425 + imagePath?: string;
  426 + }
  427 + ): Promise<void> {
  428 + // 先返回首页确保页面状态干净(避免遮罩层阻挡)
  429 + await this.gotoHome();
  430 + // 打开客户管理并搜索
  431 + await this.openCustomerManagement();
  432 + await this.searchCustomer(originalName);
  433 + await this.clickCustomerItem(originalName);
  434 + await this.clickEditButton();
  435 +
  436 + // 使用清除并填写方法修改信息
  437 + await this.clearAndFillCustomerName(customerInfo.name);
  438 + await this.clearAndFillPhone(customerInfo.phone);
  439 + await this.clearAndFillIdCard(customerInfo.idCard);
  440 +
  441 + if (options?.creditLimit) {
  442 + // 清除并填写赊欠额度(使用 force: true 绕过遮罩层)
  443 + await this.creditLimitInput.click({ force: true });
  444 + 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 });
  445 + await this.creditLimitInput.fill(options.creditLimit);
  446 + }
  447 +
  448 + if (options?.licensePlate) {
  449 + await this.fillLicensePlate(options.licensePlate);
  450 + }
  451 +
  452 + if (options?.province && options?.city && options?.district) {
  453 + await this.selectRegion(options.province, options.city, options.district);
  454 + }
  455 +
  456 + if (customerInfo.detailedAddress) {
  457 + await this.fillDetailedAddress(customerInfo.detailedAddress);
  458 + }
  459 +
  460 + if (options?.imagePath) {
  461 + await this.uploadCustomerImage(options.imagePath);
  462 + }
  463 +
  464 + await this.saveButton.click();
  465 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  466 + }
  467 +
  468 + /**
  469 + * 删除客户
  470 + * @param customerName 客户名称
  471 + */
  472 + async deleteCustomer(customerName: string): Promise<void> {
  473 + // 先返回首页确保页面状态干净(避免遮罩层阻挡)
  474 + await this.gotoHome();
  475 + await this.openCustomerManagement();
  476 + await this.searchCustomer(customerName);
  477 + await this.clickCustomerItem(customerName);
  478 + await this.clickDeleteButton();
  479 + await this.confirmDelete();
  480 + }
  481 +
  482 + /**
  483 + * 验证客户是否已被删除
  484 + * @param customerName 客户名称
  485 + */
  486 + async verifyCustomerDeleted(customerName: string): Promise<boolean> {
  487 + await this.gotoHome();
  488 + await this.openCustomerManagement();
  489 + await this.searchCustomer(customerName);
  490 +
  491 + const count = await this.page.getByText(customerName).count();
  492 + return count === 0;
  493 + }
  494 +
  495 + // ==================== 录入欠款相关方法 ====================
  496 +
  497 + /**
  498 + * 点击录入欠款按钮
  499 + */
  500 + async clickRecordDebt(): Promise<void> {
  501 + await this.page.getByText('录入欠款').click();
  502 + await this.page.waitForTimeout(500);
  503 + }
  504 +
  505 + /**
  506 + * 选择欠款类型
  507 + * @param typeIndex 类型索引(默认选择第三个,索引为2)
  508 + */
  509 + async selectDebtType(typeIndex: number = 2): Promise<void> {
  510 + 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();
  511 + await this.page.locator('uni-view').filter({ hasText: /^2$/ }).nth(typeIndex).click();
  512 + }
  513 +
  514 + /**
  515 + * 填写欠款金额
  516 + * @param amount 欠款金额
  517 + */
  518 + async fillDebtAmount(amount: string): Promise<void> {
  519 + await this.page.getByRole('spinbutton').click();
  520 + await this.page.getByRole('spinbutton').fill(amount);
  521 + }
  522 +
  523 + /**
  524 + * 填写欠款备注
  525 + * @param remark 备注
  526 + */
  527 + async fillDebtRemark(remark: string): Promise<void> {
  528 + await this.page.getByRole('textbox').nth(4).click();
  529 + await this.page.getByRole('textbox').nth(4).fill(remark);
  530 + }
  531 +
  532 + /**
  533 + * 保存欠款
  534 + */
  535 + async saveDebt(): Promise<void> {
  536 + await this.page.getByText('保存', { exact: true }).click();
  537 + await this.page.waitForLoadState('networkidle', { timeout: 30000 });
  538 + }
  539 +
  540 + /**
  541 + * 录入欠款 - 完整流程
  542 + * @param customerName 客户名称
  543 + * @param amount 欠款金额
  544 + * @param options 可选配置
  545 + */
  546 + async recordDebt(
  547 + customerName: string,
  548 + amount: string,
  549 + options?: {
  550 + typeIndex?: number; // 欠款类型索引
  551 + remark?: string; // 备注
  552 + imagePath?: string; // 图片路径
  553 + }
  554 + ): Promise<void> {
  555 + // 先返回首页确保页面状态干净(避免遮罩层阻挡)
  556 + await this.gotoHome();
  557 + // 打开客户管理并搜索
  558 + await this.openCustomerManagement();
  559 + await this.searchCustomer(customerName);
  560 + await this.clickCustomerItem(customerName);
  561 +
  562 + // 点击录入欠款
  563 + await this.clickRecordDebt();
  564 +
  565 + // 选择欠款类型
  566 + if (options?.typeIndex !== undefined) {
  567 + await this.selectDebtType(options.typeIndex);
  568 + }
  569 +
  570 + // 填写欠款金额
  571 + await this.fillDebtAmount(amount);
  572 +
  573 + // 填写备注
  574 + if (options?.remark) {
  575 + await this.fillDebtRemark(options.remark);
  576 + }
  577 +
  578 + // 上传图片 - 使用基类封装的方法
  579 + if (options?.imagePath) {
  580 + await this.uploadCustomerImage(options.imagePath);
  581 + }
  582 +
  583 + // 保存
  584 + await this.saveDebt();
  585 + }
  586 +
  587 + /**
  588 + * 验证欠款是否录入成功
  589 + * @param amount 欠款金额
  590 + */
  591 + async verifyDebtRecorded(amount: string): Promise<boolean> {
  592 + try {
  593 + await this.page.getByText(`欠款:${amount}.00元`).waitFor({ timeout: 10000 });
  594 + return true;
  595 + } catch {
  596 + return false;
  597 + }
  598 + }
  599 +}
pages/loginPage.ts
@@ -131,16 +131,20 @@ export class LoginPage extends BasePage { @@ -131,16 +131,20 @@ export class LoginPage extends BasePage {
131 /** 131 /**
132 * 完整的登录流程(半自动) 132 * 完整的登录流程(半自动)
133 * @param phone 手机号 133 * @param phone 手机号
134 - * @param userName 期望的用户名(用于验证登录成功 134 + * @param userName 期望的用户名(用于验证登录成功,可选,默认从环境变量 TEST_USER_NAME 获取
135 * @param authFilePath 认证文件保存路径 135 * @param authFilePath 认证文件保存路径
136 */ 136 */
137 async performLogin( 137 async performLogin(
138 - phone: string,  
139 - userName: string = '赵xt', 138 + phone: string,
  139 + userName?: string,
140 authFilePath: string = 'auth.json' 140 authFilePath: string = 'auth.json'
141 ): Promise<void> { 141 ): Promise<void> {
  142 + const finalUserName = userName ?? process.env.TEST_USER_NAME;
  143 + if (!finalUserName) {
  144 + throw new Error('userName 参数未提供且 TEST_USER_NAME 环境变量未设置,请设置其中之一后再运行');
  145 + }
142 await this.loginWithPhone(phone); 146 await this.loginWithPhone(phone);
143 - await this.waitForLoginSuccess(userName); 147 + await this.waitForLoginSuccess(finalUserName);
144 await this.saveAuthState(authFilePath); 148 await this.saveAuthState(authFilePath);
145 } 149 }
146 } 150 }
147 \ No newline at end of file 151 \ No newline at end of file
scripts/save-auth.ts
1 // 半自动登录,登录存放auth.json 1 // 半自动登录,登录存放auth.json
2 import { test, expect } from '@playwright/test'; 2 import { test, expect } from '@playwright/test';
3 3
  4 +// 从环境变量获取配置
  5 +const TEST_PHONE = process.env.phone;
  6 +const TEST_USER_NAME = process.env.TEST_USER_NAME;
  7 +
  8 +if (!TEST_PHONE) {
  9 + throw new Error('phone 环境变量未设置,请设置 phone 后再运行');
  10 +}
  11 +if (!TEST_USER_NAME) {
  12 + throw new Error('TEST_USER_NAME 环境变量未设置,请设置 TEST_USER_NAME 后再运行');
  13 +}
  14 +
4 test('半自动登录(自动填手机号,手动输验证码)', async ({ page }) => { 15 test('半自动登录(自动填手机号,手动输验证码)', async ({ page }) => {
5 await page.goto('/#/pages/login/index'); 16 await page.goto('/#/pages/login/index');
6 await page.getByText('手机号登录/注册').click(); 17 await page.getByText('手机号登录/注册').click();
7 await page.getByText('确定').click(); 18 await page.getByText('确定').click();
8 // --- 这里执行登录操作 --- 19 // --- 这里执行登录操作 ---
9 await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').click(); 20 await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').click();
10 - await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').fill('13548301969'); 21 + await page.locator('uni-input').filter({ hasText: '请输入手机号' }).getByRole('textbox').fill(TEST_PHONE);
11 // 此处假设出现了验证码,你需要手动输入或处理 22 // 此处假设出现了验证码,你需要手动输入或处理
12 await page.locator('uni-button').filter({ hasText: '获取验证码' }).click(); 23 await page.locator('uni-button').filter({ hasText: '获取验证码' }).click();
13 console.log('请手动输入验证码,然后点击登录按钮...'); 24 console.log('请手动输入验证码,然后点击登录按钮...');
14 await page.locator('uni-button').filter({ hasText: '登录' }).click(); 25 await page.locator('uni-button').filter({ hasText: '登录' }).click();
15 // --- 等待登录成功,跳转到首页 --- 26 // --- 等待登录成功,跳转到首页 ---
16 - await expect(page.getByText('赵xt')).toBeVisible({ timeout: 0 }); 27 + await expect(page.getByText(TEST_USER_NAME)).toBeVisible({ timeout: 0 });
17 28
18 // 4. 保存存储状态(Cookie 和 LocalStorage) 29 // 4. 保存存储状态(Cookie 和 LocalStorage)
19 await page.context().storageState({ path: 'auth.json' }); 30 await page.context().storageState({ path: 'auth.json' });
tests/customer.spec.ts
1 import { test, expect } from '../fixtures'; 1 import { test, expect } from '../fixtures';
2 -import { generateCustomerName, generateIdCard, generatePhoneNumber, generateDetailedAddress, generateCustomerInfo, getRandomImage } from '../utils/dataGenerator'; 2 +import { generateCustomerName, generateIdCard, generatePhoneNumber, generateDetailedAddress, generateCustomerInfo, getRandomImage, generateRemark, generateAmount } from '../utils/dataGenerator';
3 import * as allure from 'allure-js-commons'; 3 import * as allure from 'allure-js-commons';
4 4
5 /** 5 /**
6 * 客户管理测试 6 * 客户管理测试
7 */ 7 */
  8 +// 新增客户
8 test.describe('客户管理', () => { 9 test.describe('客户管理', () => {
9 // 使用已保存的认证状态 10 // 使用已保存的认证状态
10 test.use({ storageState: 'auth.json' }); 11 test.use({ storageState: 'auth.json' });
@@ -61,75 +62,145 @@ test.describe(&#39;客户管理&#39;, () =&gt; { @@ -61,75 +62,145 @@ test.describe(&#39;客户管理&#39;, () =&gt; {
61 }); 62 });
62 }); 63 });
63 64
64 -// test('新增客户 - 仅必填信息', async ({ customerPage }, testInfo) => { 65 +// test('修改客户', async ({ customerPage }, testInfo) => {
65 // // 添加allure元素 66 // // 添加allure元素
66 // await allure.epic('客户管理'); 67 // await allure.epic('客户管理');
67 // await allure.feature('客户信息'); 68 // await allure.feature('客户信息');
68 -// await allure.story('创建新客户(仅必填信息)'); 69 +// await allure.story('修改客户');
69 70
70 -// // 步骤1:生成随机客户信息  
71 -// const customerInfo = await allure.step('生成随机客户信息', async (step) => {  
72 -// const info = generateCustomerInfo();  
73 -// console.log('生成的客户信息:', info);  
74 -// return info;  
75 -// }); 71 +// // 先创建一个客户用于修改
  72 +// const originalName = generateCustomerName();
  73 +// const originalPhone = generatePhoneNumber();
  74 +// const originalIdCard = generateIdCard();
  75 +
  76 +// console.log('原客户名称:', originalName);
76 77
77 -// // 步骤2:执行新增客户操作  
78 -// await allure.step('填写并提交客户表单(仅必填信息)', async () => { 78 +// // 步骤1:先创建客户
  79 +// await allure.step('创建待修改的客户', async () => {
79 // await customerPage.gotoHome(); 80 // await customerPage.gotoHome();
80 // await customerPage.createCustomer({ 81 // await customerPage.createCustomer({
81 -// name: customerInfo.name,  
82 -// phone: customerInfo.phone,  
83 -// idCard: customerInfo.idCard, 82 +// name: originalName,
  83 +// phone: originalPhone,
  84 +// idCard: originalIdCard,
84 // }); 85 // });
85 -// await customerPage.attachScreenshot(testInfo, '新增客户成功截图');  
86 // }); 86 // });
87 87
88 -// // 步骤3:验证客户创建成功  
89 -// await allure.step('验证客户创建成功', async () => {  
90 -// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name);  
91 -// expect(isCreated).toBeTruthy(); 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();
92 // }); 121 // });
93 // }); 122 // });
94 123
95 -// test('新增客户 - 带详细地址', async ({ customerPage }, testInfo) => { 124 +// test('删除客户', async ({ customerPage }, testInfo) => {
96 // // 添加allure元素 125 // // 添加allure元素
97 // await allure.epic('客户管理'); 126 // await allure.epic('客户管理');
98 // await allure.feature('客户信息'); 127 // await allure.feature('客户信息');
99 -// await allure.story('创建新客户(带详细地址)');  
100 -  
101 -// // 步骤1:生成随机客户信息  
102 -// const customerInfo = await allure.step('生成随机客户信息', async (step) => {  
103 -// const name = generateCustomerName();  
104 -// const phone = generatePhoneNumber();  
105 -// const idCard = generateIdCard();  
106 -// const detailedAddress = generateDetailedAddress();  
107 -  
108 -// console.log('生成的客户信息:', { name, phone, idCard, detailedAddress });  
109 -  
110 -// return { name, phone, idCard, detailedAddress }; 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, '删除客户成功截图');
111 // }); 151 // });
112 152
113 -// // 步骤2:执行新增客户操作  
114 -// await allure.step('填写并提交客户表单', async () => { 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 () => {
115 // await customerPage.gotoHome(); 175 // await customerPage.gotoHome();
116 -// await customerPage.createCustomer(  
117 -// {  
118 -// name: customerInfo.name,  
119 -// phone: customerInfo.phone,  
120 -// idCard: customerInfo.idCard,  
121 -// },  
122 -// {  
123 -// creditLimit: '10000',  
124 -// }  
125 -// );  
126 -// await customerPage.attachScreenshot(testInfo, '新增客户成功截图'); 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, '录入欠款成功截图');
127 // }); 198 // });
128 199
129 -// // 步骤3:验证客户创建成功  
130 -// await allure.step('验证客户创建成功', async () => {  
131 -// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name);  
132 -// expect(isCreated).toBeTruthy(); 200 +// // 步骤3:验证欠款录入成功
  201 +// await allure.step('验证欠款录入成功', async () => {
  202 +// const isRecorded = await customerPage.verifyDebtRecorded(debtAmount);
  203 +// expect(isRecorded).toBeTruthy();
133 // }); 204 // });
134 // }); 205 // });
135 }); 206 });
136 \ No newline at end of file 207 \ No newline at end of file
tests/login.setup.ts
@@ -3,6 +3,18 @@ import path from &#39;path&#39;; @@ -3,6 +3,18 @@ import path from &#39;path&#39;;
3 3
4 const authFile = path.join(__dirname, '../auth.json'); 4 const authFile = path.join(__dirname, '../auth.json');
5 5
  6 +// 从环境变量获取配置
  7 +const BASE_URL = process.env.BASE_URL;
  8 +const TEST_USER_NAME = process.env.TEST_USER_NAME;
  9 +
  10 +if (!BASE_URL) {
  11 + throw new Error('BASE_URL 环境变量未设置,请设置 BASE_URL 后再运行');
  12 +}
  13 +
  14 +if (!TEST_USER_NAME) {
  15 + throw new Error('TEST_USER_NAME 环境变量未设置,请设置 TEST_USER_NAME 后再运行');
  16 +}
  17 +
6 setup('authenticate', async ({ page }) => { 18 setup('authenticate', async ({ page }) => {
7 // 1. 先检查 auth.json 是否存在 19 // 1. 先检查 auth.json 是否存在
8 // 如果存在,并且你想让它自动跳过,可以加一个判断。这里简单起见,总是执行手动登录。 20 // 如果存在,并且你想让它自动跳过,可以加一个判断。这里简单起见,总是执行手动登录。
@@ -11,13 +23,13 @@ setup(&#39;authenticate&#39;, async ({ page }) =&gt; { @@ -11,13 +23,13 @@ setup(&#39;authenticate&#39;, async ({ page }) =&gt; {
11 console.log('开始执行认证设置...'); 23 console.log('开始执行认证设置...');
12 24
13 // 2. 访问登录页 25 // 2. 访问登录页
14 - await page.goto('https://erp-pad.test.gszdtop.com/#/'); 26 + await page.goto(`${BASE_URL}/#/`);
15 27
16 // 3. 手动登录(这里可以加一些提示) 28 // 3. 手动登录(这里可以加一些提示)
17 //console.log('请手动完成手机验证码登录...'); 29 //console.log('请手动完成手机验证码登录...');
18 30
19 // 4. 等待登录成功,检测某个登录后才会出现的元素 31 // 4. 等待登录成功,检测某个登录后才会出现的元素
20 - await expect(page.getByText('赵xt')).toBeVisible(); 32 + await expect(page.getByText(TEST_USER_NAME)).toBeVisible();
21 // await page.waitForSelector('text=个人中心', { timeout: 60000 }); 33 // await page.waitForSelector('text=个人中心', { timeout: 60000 });
22 34
23 // 5. 登录成功后,保存状态 35 // 5. 登录成功后,保存状态
utils/dataGenerator.ts
@@ -173,3 +173,32 @@ export function getRandomImage(imageDir: string = &#39;test-data/img&#39;): string | nul @@ -173,3 +173,32 @@ export function getRandomImage(imageDir: string = &#39;test-data/img&#39;): string | nul
173 return null; 173 return null;
174 } 174 }
175 } 175 }
  176 +
  177 +// 随机生成备注(50字以内)
  178 +export function generateRemark(maxLength: number = 50): string {
  179 + const phrases = [
  180 + '自动化测试数据',
  181 + '测试备注信息',
  182 + '系统自动生成',
  183 + '功能验证数据',
  184 + '接口测试记录',
  185 + '数据录入测试',
  186 + '业务流程验证',
  187 + '质量保证测试',
  188 + '回归测试数据',
  189 + '冒烟测试记录'
  190 + ];
  191 +
  192 + const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
  193 + const timestamp = Date.now().toString().slice(-6);
  194 + const remark = `${randomPhrase}_${timestamp}`;
  195 +
  196 + // 确保不超过最大长度
  197 + return remark.slice(0, maxLength);
  198 +}
  199 +
  200 +// 随机生成金额(1-9999)
  201 +export function generateAmount(min: number = 1, max: number = 9999): string {
  202 + const amount = Math.floor(Math.random() * (max - min + 1)) + min;
  203 + return amount.toString();
  204 +}