Commit 140ac106a403710398d8315d427bace0e7c2a92d
1 parent
33d656b8
新增客户管理
Showing
14 changed files
with
715 additions
and
52 deletions
fixtures/testFixture.ts
| @@ -3,6 +3,7 @@ import { LoginPage } from '../pages/loginPage'; | @@ -3,6 +3,7 @@ import { LoginPage } from '../pages/loginPage'; | ||
| 3 | import { ProductPage } from '../pages/productPage'; | 3 | import { ProductPage } from '../pages/productPage'; |
| 4 | import { SalePage } from '../pages/salePage'; | 4 | import { SalePage } from '../pages/salePage'; |
| 5 | import { ConsignmentPage } from '../pages/consignmentPage'; | 5 | import { ConsignmentPage } from '../pages/consignmentPage'; |
| 6 | +import { CustomerPage } from '../pages/customerPage'; | ||
| 6 | 7 | ||
| 7 | /** | 8 | /** |
| 8 | * 页面对象夹具类型定义 | 9 | * 页面对象夹具类型定义 |
| @@ -27,6 +28,11 @@ export type PageFixtures = { | @@ -27,6 +28,11 @@ export type PageFixtures = { | ||
| 27 | * 代销入库页面 | 28 | * 代销入库页面 |
| 28 | */ | 29 | */ |
| 29 | consignmentPage: ConsignmentPage; | 30 | consignmentPage: ConsignmentPage; |
| 31 | + | ||
| 32 | + /** | ||
| 33 | + * 客户管理页面 | ||
| 34 | + */ | ||
| 35 | + customerPage: CustomerPage; | ||
| 30 | }; | 36 | }; |
| 31 | 37 | ||
| 32 | /** | 38 | /** |
| @@ -52,6 +58,11 @@ export const test = base.extend<PageFixtures>({ | @@ -52,6 +58,11 @@ export const test = base.extend<PageFixtures>({ | ||
| 52 | consignmentPage: async ({ page }, use) => { | 58 | consignmentPage: async ({ page }, use) => { |
| 53 | await use(new ConsignmentPage(page)); | 59 | await use(new ConsignmentPage(page)); |
| 54 | }, | 60 | }, |
| 61 | + | ||
| 62 | + // 客户管理页面 | ||
| 63 | + customerPage: async ({ page }, use) => { | ||
| 64 | + await use(new CustomerPage(page)); | ||
| 65 | + }, | ||
| 55 | }); | 66 | }); |
| 56 | 67 | ||
| 57 | /** | 68 | /** |
| @@ -85,4 +96,8 @@ export const fullTest = authTest.extend<PageFixtures>({ | @@ -85,4 +96,8 @@ export const fullTest = authTest.extend<PageFixtures>({ | ||
| 85 | consignmentPage: async ({ authenticatedPage }, use) => { | 96 | consignmentPage: async ({ authenticatedPage }, use) => { |
| 86 | await use(new ConsignmentPage(authenticatedPage)); | 97 | await use(new ConsignmentPage(authenticatedPage)); |
| 87 | }, | 98 | }, |
| 88 | -}); | ||
| 89 | \ No newline at end of file | 99 | \ No newline at end of file |
| 100 | + customerPage: async ({authenticatedPage }, use) =>{ | ||
| 101 | + await use(new CustomerPage(authenticatedPage)); | ||
| 102 | + }, | ||
| 103 | + | ||
| 104 | + }); | ||
| 90 | \ No newline at end of file | 105 | \ No newline at end of file |
pages/consignmentPage.ts
| @@ -169,6 +169,37 @@ export class ConsignmentPage extends BasePage { | @@ -169,6 +169,37 @@ export class ConsignmentPage extends BasePage { | ||
| 169 | await this.productListButton.click(); | 169 | await this.productListButton.click(); |
| 170 | } | 170 | } |
| 171 | 171 | ||
| 172 | + // /** | ||
| 173 | + // * 获取商品列表中所有可用的商品名称 | ||
| 174 | + // * @returns 商品名称数组 | ||
| 175 | + // */ | ||
| 176 | + // async getAvailableProducts(): Promise<string[]> { | ||
| 177 | + // // 等待商品列表加载 | ||
| 178 | + // await this.page.locator('.productName').first().waitFor({ state: 'visible', timeout: 5000 }); | ||
| 179 | + | ||
| 180 | + // // 获取所有商品名称 | ||
| 181 | + // const productElements = await this.page.locator('.productName').allTextContents(); | ||
| 182 | + // return productElements.filter(name => name.trim() !== ''); | ||
| 183 | + // } | ||
| 184 | + | ||
| 185 | + // /** | ||
| 186 | + // * 随机选择一个商品 | ||
| 187 | + // * @returns 随机选择的商品名称 | ||
| 188 | + // */ | ||
| 189 | + // async selectRandomProduct(): Promise<string> { | ||
| 190 | + // const products = await this.getAvailableProducts(); | ||
| 191 | + | ||
| 192 | + // if (products.length === 0) { | ||
| 193 | + // throw new Error('商品列表为空,无法选择商品'); | ||
| 194 | + // } | ||
| 195 | + | ||
| 196 | + // const randomIndex = Math.floor(Math.random() * products.length); | ||
| 197 | + // const selectedProduct = products[randomIndex]; | ||
| 198 | + // console.log(`随机选择的商品: ${selectedProduct}`); | ||
| 199 | + | ||
| 200 | + // return selectedProduct; | ||
| 201 | + // } | ||
| 202 | + | ||
| 172 | /** | 203 | /** |
| 173 | * 选择商品 | 204 | * 选择商品 |
| 174 | * @param productName 商品名称 | 205 | * @param productName 商品名称 |
pages/customerPage.ts
0 → 100644
| 1 | +import { Page, Locator, expect } from '@playwright/test'; | ||
| 2 | +import { BasePage } from './basePage'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 客户管理页面选择器 | ||
| 6 | + */ | ||
| 7 | +const selectors = { | ||
| 8 | + // 导航 | ||
| 9 | + customerMenu: 'text=客户管理', | ||
| 10 | + addCustomerButton: 'text=新增客户', | ||
| 11 | + | ||
| 12 | + // 表单字段 | ||
| 13 | + customerNameInput: 'textbox', // 客户名称输入框 | ||
| 14 | + phoneInput: 'spinbutton', // 手机号输入框 | ||
| 15 | + idCardInput: 'textbox', // 身份证输入框 | ||
| 16 | + | ||
| 17 | + // 客户分组 | ||
| 18 | + customerGroupPicker: '.nut-input__mask', | ||
| 19 | + normalCustomerOption: 'uni-view:has-text("普通客户")', | ||
| 20 | + | ||
| 21 | + // 赊欠额度 | ||
| 22 | + creditLimitSwitch: '.nut-form-item__body__slots > .nut-switch', | ||
| 23 | + creditLimitInput: 'uni-input:has-text("请输入赊欠额度/不输为不限额") input', | ||
| 24 | + | ||
| 25 | + // 车牌号 | ||
| 26 | + // licensePlateInput: '.nut-input__mask', | ||
| 27 | + licensePlateInput:'uni-view:nth-child(11) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask', | ||
| 28 | + confirmLicenceButton:'text=确定', | ||
| 29 | + // 省市区选择 | ||
| 30 | + regionPicker: 'uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask', | ||
| 31 | + confirmRegionButton: 'text=确认选择', | ||
| 32 | + | ||
| 33 | + // 详细地址 | ||
| 34 | + detailedAddressInput: 'uni-view:nth-child(13) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input', | ||
| 35 | + | ||
| 36 | + // 操作按钮 | ||
| 37 | + confirmButton: 'text=确定', | ||
| 38 | + saveButton: 'text=保存', | ||
| 39 | + | ||
| 40 | + // 验证 | ||
| 41 | + customerInList: (customerName: string) => `uni-view:has-text("${customerName}")`, | ||
| 42 | +}; | ||
| 43 | + | ||
| 44 | +/** | ||
| 45 | + * 客户管理页面类 | ||
| 46 | + * 处理客户管理相关操作 | ||
| 47 | + */ | ||
| 48 | +export class CustomerPage extends BasePage { | ||
| 49 | + // 导航定位器 | ||
| 50 | + readonly customerMenu: Locator; | ||
| 51 | + readonly addCustomerButton: Locator; | ||
| 52 | + | ||
| 53 | + // 表单字段 | ||
| 54 | + readonly customerNameInput: Locator; | ||
| 55 | + readonly phoneInput: Locator; | ||
| 56 | + readonly idCardInput: Locator; | ||
| 57 | + | ||
| 58 | + // 客户分组 | ||
| 59 | + readonly customerGroupPicker: Locator; | ||
| 60 | + readonly normalCustomerOption: Locator; | ||
| 61 | + | ||
| 62 | + // 赊欠额度 | ||
| 63 | + readonly creditLimitSwitch: Locator; | ||
| 64 | + readonly creditLimitInput: Locator; | ||
| 65 | + | ||
| 66 | + // 车牌号 | ||
| 67 | + readonly licensePlateInput: Locator; | ||
| 68 | + | ||
| 69 | + // 省市区选择 | ||
| 70 | + readonly regionPicker: Locator; | ||
| 71 | + readonly confirmRegionButton: Locator; | ||
| 72 | + | ||
| 73 | + // 详细地址 | ||
| 74 | + readonly detailedAddressInput: Locator; | ||
| 75 | + | ||
| 76 | + // 操作按钮 | ||
| 77 | + readonly confirmButton: Locator; | ||
| 78 | + readonly saveButton: Locator; | ||
| 79 | + | ||
| 80 | + constructor(page: Page) { | ||
| 81 | + super(page); | ||
| 82 | + | ||
| 83 | + this.customerMenu = page.getByText('客户管理'); | ||
| 84 | + this.addCustomerButton = page.getByText('新增客户'); | ||
| 85 | + this.customerNameInput = page.getByRole('textbox').nth(1); | ||
| 86 | + this.phoneInput = page.getByRole('spinbutton').first(); | ||
| 87 | + this.idCardInput = page.getByRole('textbox').nth(2); | ||
| 88 | + this.customerGroupPicker = page.locator('.nut-input__mask').first(); | ||
| 89 | + this.normalCustomerOption = page.locator('uni-view').filter({ hasText: /^普通客户$/ }).nth(3); | ||
| 90 | + this.creditLimitSwitch = page.locator('.nut-form-item__body__slots > .nut-switch'); | ||
| 91 | + this.creditLimitInput = page.locator('uni-input').filter({ hasText: '请输入赊欠额度/不输为不限额' }).getByRole('spinbutton'); | ||
| 92 | + this.licensePlateInput = page.locator('.nut-input__mask').first(); | ||
| 93 | + this.regionPicker = page.locator('uni-view:nth-child(11) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask'); | ||
| 94 | + this.confirmRegionButton = page.getByText('确认选择'); | ||
| 95 | + this.detailedAddressInput = page.locator('uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input'); | ||
| 96 | + this.confirmButton = page.getByText('确定'); | ||
| 97 | + this.saveButton = page.getByText('保存'); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /** | ||
| 101 | + * 导航到首页并等待加载 | ||
| 102 | + */ | ||
| 103 | + async gotoHome(): Promise<void> { | ||
| 104 | + await this.navigate('/'); | ||
| 105 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + /** | ||
| 109 | + * 打开客户管理页面 | ||
| 110 | + */ | ||
| 111 | + async openCustomerManagement(): Promise<void> { | ||
| 112 | + await this.customerMenu.click(); | ||
| 113 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + /** | ||
| 117 | + * 点击新增客户按钮 | ||
| 118 | + */ | ||
| 119 | + async clickAddCustomer(): Promise<void> { | ||
| 120 | + await this.addCustomerButton.click(); | ||
| 121 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + /** | ||
| 125 | + * 填写客户名称 | ||
| 126 | + * @param name 客户名称 | ||
| 127 | + */ | ||
| 128 | + async fillCustomerName(name: string): Promise<void> { | ||
| 129 | + await this.customerNameInput.click(); | ||
| 130 | + await this.customerNameInput.fill(name); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + /** | ||
| 134 | + * 填写手机号 | ||
| 135 | + * @param phone 手机号 | ||
| 136 | + */ | ||
| 137 | + async fillPhone(phone: string): Promise<void> { | ||
| 138 | + await this.phoneInput.click(); | ||
| 139 | + await this.phoneInput.fill(phone); | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + /** | ||
| 143 | + * 填写身份证号 | ||
| 144 | + * @param idCard 身份证号 | ||
| 145 | + */ | ||
| 146 | + async fillIdCard(idCard: string): Promise<void> { | ||
| 147 | + await this.idCardInput.click(); | ||
| 148 | + await this.idCardInput.fill(idCard); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + /** | ||
| 152 | + * 选择客户分组(选择"普通客户") | ||
| 153 | + */ | ||
| 154 | + async selectCustomerGroup(): Promise<void> { | ||
| 155 | + // 点击客户分组选择器 | ||
| 156 | + await this.customerGroupPicker.click(); | ||
| 157 | + | ||
| 158 | + // 等待弹窗出现 | ||
| 159 | + await this.page.waitForTimeout(500); | ||
| 160 | + | ||
| 161 | + // 选择"普通客户"(使用 nth(4) 和 force: true 绕过遮罩层) | ||
| 162 | + await this.page.locator('uni-view').filter({ hasText: /^普通客户$/ }).nth(4).click({ force: true }); | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + /** | ||
| 166 | + * 开启赊欠额度开关 | ||
| 167 | + */ | ||
| 168 | + async enableCreditLimit(): Promise<void> { | ||
| 169 | + await this.creditLimitSwitch.click(); | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + /** | ||
| 173 | + * 填写赊欠额度 | ||
| 174 | + * @param amount 赊欠额度金额 | ||
| 175 | + */ | ||
| 176 | + async fillCreditLimit(amount: string): Promise<void> { | ||
| 177 | + await this.creditLimitInput.click(); | ||
| 178 | + await this.creditLimitInput.fill(amount); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + /** | ||
| 182 | + * 填写车牌号 | ||
| 183 | + * @param licensePlate 车牌号(如:渝ZY0706) | ||
| 184 | + */ | ||
| 185 | + async fillLicensePlate(licensePlate: string): Promise<void> { | ||
| 186 | + // 等待页面稳定 | ||
| 187 | + await this.page.waitForTimeout(500); | ||
| 188 | + | ||
| 189 | + // 点击车牌号输入框 - nth-child(11),开启赊欠额度后位置 | ||
| 190 | + await this.page.locator('uni-view:nth-child(11) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask').click(); | ||
| 191 | + | ||
| 192 | + // 等待车牌键盘面板加载完成 | ||
| 193 | + await this.page.waitForTimeout(1000); | ||
| 194 | + | ||
| 195 | + // 解析车牌号的省份和字母部分 | ||
| 196 | + const province = licensePlate.charAt(0); // 如:渝 | ||
| 197 | + const letter = licensePlate.charAt(1); // 如:Z | ||
| 198 | + const numbers = licensePlate.substring(2); // 如:Y0706 | ||
| 199 | + | ||
| 200 | + // 点击省份 | ||
| 201 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${province}$`) }).click(); | ||
| 202 | + | ||
| 203 | + // 等待字母键盘出现 | ||
| 204 | + await this.page.waitForTimeout(300); | ||
| 205 | + | ||
| 206 | + // 点击第一个字母(需要用 nth(1) 选择第二个匹配项) | ||
| 207 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${letter}$`) }).nth(1).click(); | ||
| 208 | + | ||
| 209 | + // 逐个点击车牌号码 | ||
| 210 | + for (let i = 0; i < numbers.length; i++) { | ||
| 211 | + const char = numbers[i]; | ||
| 212 | + await this.page.waitForTimeout(100); | ||
| 213 | + if (char.match(/[A-Za-z]/)) { | ||
| 214 | + // 字母 | ||
| 215 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char.toUpperCase()}$`) }).click(); | ||
| 216 | + } else if (char.match(/[0-9]/)) { | ||
| 217 | + // 数字 - 检查是否需要使用 nth(1) | ||
| 218 | + const digitCount = await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) }).count(); | ||
| 219 | + if (digitCount > 1) { | ||
| 220 | + // 如果有多个相同数字,点击第二个 | ||
| 221 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) }).nth(1).click(); | ||
| 222 | + } else { | ||
| 223 | + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) }).click(); | ||
| 224 | + } | ||
| 225 | + } | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + // 点击确定按钮 | ||
| 229 | + await this.page.getByText('确定').click(); | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + /** | ||
| 233 | + * 选择省市区 | ||
| 234 | + * @param province 省份(如:山西省) | ||
| 235 | + * @param city 城市(如:长治市) | ||
| 236 | + * @param district 区县(如:潞城区) | ||
| 237 | + */ | ||
| 238 | + async selectRegion(province: string, city: string, district: string): Promise<void> { | ||
| 239 | + // 点击省市区选择器 - nth-child(12),开启赊欠额度后位置 | ||
| 240 | + await this.page.locator('uni-view:nth-child(12) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask').click(); | ||
| 241 | + | ||
| 242 | + // 等待弹窗出现 | ||
| 243 | + await this.page.waitForTimeout(500); | ||
| 244 | + | ||
| 245 | + // 选择省份 | ||
| 246 | + await this.page.getByText(province).click(); | ||
| 247 | + | ||
| 248 | + // 选择城市 | ||
| 249 | + await this.page.getByText(city).click(); | ||
| 250 | + | ||
| 251 | + // 选择区县 | ||
| 252 | + await this.page.getByText(district).click(); | ||
| 253 | + | ||
| 254 | + // 确认选择 | ||
| 255 | + await this.page.getByText('确认选择').click(); | ||
| 256 | + } | ||
| 257 | + | ||
| 258 | + /** | ||
| 259 | + * 填写详细地址 | ||
| 260 | + * @param address 详细地址 | ||
| 261 | + */ | ||
| 262 | + async fillDetailedAddress(address: string): Promise<void> { | ||
| 263 | + // 点击详细地址输入框 - nth-child(13),开启赊欠额度后位置 | ||
| 264 | + await this.page.locator('uni-view:nth-child(13) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input').click(); | ||
| 265 | + // 填写详细地址 | ||
| 266 | + await this.page.locator('uni-view:nth-child(13) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__input > .uni-input-wrapper > .uni-input-input').fill(address); | ||
| 267 | + } | ||
| 268 | + | ||
| 269 | + /** | ||
| 270 | + * 上传客户图片 | ||
| 271 | + * @param imagePath 图片文件路径(相对于项目根目录) | ||
| 272 | + */ | ||
| 273 | + async uploadCustomerImage(imagePath: string): Promise<void> { | ||
| 274 | + // 使用 Playwright 的 file_chooser 事件处理文件上传 | ||
| 275 | + // 监听文件选择器事件 | ||
| 276 | + const fileChooserPromise = this.page.waitForEvent('filechooser'); | ||
| 277 | + | ||
| 278 | + // 点击上传按钮触发文件选择器 | ||
| 279 | + await this.page.locator('uni-scroll-view uni-button').click(); | ||
| 280 | + | ||
| 281 | + // 等待文件选择器出现并设置文件 | ||
| 282 | + const fileChooser = await fileChooserPromise; | ||
| 283 | + await fileChooser.setFiles(imagePath); | ||
| 284 | + | ||
| 285 | + // 等待上传完成 | ||
| 286 | + await this.page.waitForTimeout(1000); | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + /** | ||
| 290 | + * 创建新客户 - 完整流程 | ||
| 291 | + * @param customerInfo 客户信息 | ||
| 292 | + * @param options 可选配置 | ||
| 293 | + */ | ||
| 294 | + async createCustomer( | ||
| 295 | + customerInfo: { | ||
| 296 | + name: string; | ||
| 297 | + phone: string; | ||
| 298 | + idCard: string; | ||
| 299 | + detailedAddress?: string; | ||
| 300 | + }, | ||
| 301 | + options?: { | ||
| 302 | + creditLimit?: string; | ||
| 303 | + licensePlate?: string; | ||
| 304 | + province?: string; | ||
| 305 | + city?: string; | ||
| 306 | + district?: string; | ||
| 307 | + imagePath?: string; | ||
| 308 | + } | ||
| 309 | + ): Promise<void> { | ||
| 310 | + // 打开客户管理 | ||
| 311 | + await this.openCustomerManagement(); | ||
| 312 | + | ||
| 313 | + // 点击新增客户 | ||
| 314 | + await this.clickAddCustomer(); | ||
| 315 | + | ||
| 316 | + // 填写客户名称 | ||
| 317 | + await this.fillCustomerName(customerInfo.name); | ||
| 318 | + | ||
| 319 | + // 填写手机号 | ||
| 320 | + await this.fillPhone(customerInfo.phone); | ||
| 321 | + | ||
| 322 | + // 填写身份证 | ||
| 323 | + await this.fillIdCard(customerInfo.idCard); | ||
| 324 | + | ||
| 325 | + // 选择客户分组(默认第一个) | ||
| 326 | + await this.selectCustomerGroup(); | ||
| 327 | + | ||
| 328 | + // 如果有赊欠额度配置 | ||
| 329 | + if (options?.creditLimit) { | ||
| 330 | + await this.enableCreditLimit(); | ||
| 331 | + await this.fillCreditLimit(options.creditLimit); | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + // 如果有车牌号配置 | ||
| 335 | + if (options?.licensePlate) { | ||
| 336 | + await this.fillLicensePlate(options.licensePlate); | ||
| 337 | + } | ||
| 338 | + | ||
| 339 | + // 如果有省市区配置 | ||
| 340 | + if (options?.province && options?.city && options?.district) { | ||
| 341 | + await this.selectRegion(options.province, options.city, options.district); | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + // 如果有详细地址 | ||
| 345 | + if (customerInfo.detailedAddress) { | ||
| 346 | + await this.fillDetailedAddress(customerInfo.detailedAddress); | ||
| 347 | + } | ||
| 348 | + | ||
| 349 | + // 如果有图片上传 | ||
| 350 | + if (options?.imagePath) { | ||
| 351 | + await this.uploadCustomerImage(options.imagePath); | ||
| 352 | + } | ||
| 353 | + | ||
| 354 | + // 保存客户 | ||
| 355 | + await this.saveButton.click(); | ||
| 356 | + | ||
| 357 | + // 等待保存完成 | ||
| 358 | + await this.page.waitForLoadState('networkidle', { timeout: 30000 }); | ||
| 359 | + } | ||
| 360 | + | ||
| 361 | + /** | ||
| 362 | + * 验证客户是否创建成功 | ||
| 363 | + * @param customerName 客户名称 | ||
| 364 | + */ | ||
| 365 | + async verifyCustomerCreated(customerName: string): Promise<boolean> { | ||
| 366 | + try { | ||
| 367 | + await this.page.waitForSelector(selectors.customerInList(customerName), { timeout: 10000 }); | ||
| 368 | + return true; | ||
| 369 | + } catch { | ||
| 370 | + return false; | ||
| 371 | + } | ||
| 372 | + } | ||
| 373 | +} | ||
| 0 | \ No newline at end of file | 374 | \ No newline at end of file |
test-data/img/海拉鲁.jpg
0 → 100644
7.87 KB
test-data/img/海拉鲁鲈鱼.jpg
0 → 100644
29.4 KB
test-data/img/牛肉.jpg
0 → 100644
20 KB
test-data/img/猪肉.jpg
0 → 100644
12.1 KB
test-data/img/苹果.jpg
0 → 100644
10.3 KB
test-data/img/莫利布林.jpg
0 → 100644
75.7 KB
test-data/img/角.jpg
0 → 100644
11.8 KB
test-data/img/鸭子.jpg
0 → 100644
16.6 KB
tests/consignmentOrder.spec.ts
| @@ -34,57 +34,8 @@ test.describe('代销入库', () => { | @@ -34,57 +34,8 @@ test.describe('代销入库', () => { | ||
| 34 | ); | 34 | ); |
| 35 | await consignmentPage.attachScreenshot(testInfo,'代销入库成功截图'); | 35 | await consignmentPage.attachScreenshot(testInfo,'代销入库成功截图'); |
| 36 | }) | 36 | }) |
| 37 | - // 使用页面对象创建代销入库 | ||
| 38 | - // await consignmentPage.createConsignmentOrder( | ||
| 39 | - // batchAlias, // 批次别名 | ||
| 40 | - // '娃娃菜', // 商品名称 | ||
| 41 | - // '10', // 数量 | ||
| 42 | - // '1' // 费用金额 | ||
| 43 | - // ); | 37 | + |
| 44 | }); | 38 | }); |
| 45 | 39 | ||
| 46 | - // test('新增代销入库 - 简化流程(无费用)', async ({ consignmentPage }) => { | ||
| 47 | - // // 生成唯一批次别名 | ||
| 48 | - // const batchAlias = generateOtherName('代卖'); | ||
| 49 | - // console.log('生成的批次别名:', batchAlias); | ||
| 50 | 40 | ||
| 51 | - // // 使用简化流程创建代销入库 | ||
| 52 | - // await consignmentPage.createSimpleConsignmentOrder( | ||
| 53 | - // batchAlias, // 批次别名 | ||
| 54 | - // '娃娃菜' // 商品名称 | ||
| 55 | - // ); | ||
| 56 | - | ||
| 57 | - // // 验证批次创建成功 | ||
| 58 | - // await consignmentPage.expectBatchCreated(batchAlias); | ||
| 59 | - // }); | ||
| 60 | - | ||
| 61 | - // test('新增代销入库 - 分步操作示例', async ({ consignmentPage }) => { | ||
| 62 | - // // 生成唯一批次别名 | ||
| 63 | - // const batchAlias = generateOtherName('代卖'); | ||
| 64 | - // console.log('生成的批次别名:', batchAlias); | ||
| 65 | - | ||
| 66 | - // // 导航到新增页面 | ||
| 67 | - // await consignmentPage.navigateToNewConsignment(); | ||
| 68 | - | ||
| 69 | - // // 选择仓库 | ||
| 70 | - // await consignmentPage.selectWarehouse(); | ||
| 71 | - // await consignmentPage.selectSecondWarehouse(); | ||
| 72 | - | ||
| 73 | - // // 输入批次别名 | ||
| 74 | - // await consignmentPage.enterBatchAlias(batchAlias); | ||
| 75 | - | ||
| 76 | - // // 选择商品并输入数量 | ||
| 77 | - // await consignmentPage.selectProductWithQuantity('娃娃菜', '10'); | ||
| 78 | - | ||
| 79 | - // // 添加费用 | ||
| 80 | - // await consignmentPage.addExpense(0, '1'); | ||
| 81 | - // await consignmentPage.selectPaymentMethod(0); | ||
| 82 | - | ||
| 83 | - // // 点击创建 | ||
| 84 | - // await consignmentPage.clickCreate(); | ||
| 85 | - // await consignmentPage.waitForCreationComplete(); | ||
| 86 | - | ||
| 87 | - // // 验证 | ||
| 88 | - // await consignmentPage.expectBatchCreated(batchAlias); | ||
| 89 | - // }); | ||
| 90 | }); | 41 | }); |
| 91 | \ No newline at end of file | 42 | \ No newline at end of file |
tests/customer.spec.ts
0 → 100644
| 1 | +import { test, expect } from '../fixtures'; | ||
| 2 | +import { generateCustomerName, generateIdCard, generatePhoneNumber, generateDetailedAddress, generateCustomerInfo, getRandomImage } from '../utils/dataGenerator'; | ||
| 3 | +import * as allure from 'allure-js-commons'; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 客户管理测试 | ||
| 7 | + */ | ||
| 8 | +test.describe('客户管理', () => { | ||
| 9 | + // 使用已保存的认证状态 | ||
| 10 | + test.use({ storageState: 'auth.json' }); | ||
| 11 | + | ||
| 12 | + test('新增客户', async ({ customerPage }, testInfo) => { | ||
| 13 | + // 添加allure元素 | ||
| 14 | + await allure.epic('客户管理'); | ||
| 15 | + await allure.feature('客户信息'); | ||
| 16 | + await allure.story('创建新客户'); | ||
| 17 | + | ||
| 18 | + // 步骤1:生成随机客户信息 | ||
| 19 | + const customerInfo = await allure.step('生成随机客户信息', async (step) => { | ||
| 20 | + const name = generateCustomerName(); | ||
| 21 | + const phone = generatePhoneNumber(); | ||
| 22 | + const idCard = generateIdCard(); | ||
| 23 | + const detailedAddress = generateDetailedAddress(); | ||
| 24 | + | ||
| 25 | + | ||
| 26 | + console.log('生成的客户信息:', { name, phone, idCard, detailedAddress }); | ||
| 27 | + | ||
| 28 | + return { name, phone, idCard, detailedAddress }; | ||
| 29 | + }); | ||
| 30 | + | ||
| 31 | + // 步骤2:执行新增客户操作 | ||
| 32 | + await allure.step('填写并提交客户表单', async () => { | ||
| 33 | + await customerPage.gotoHome(); | ||
| 34 | + | ||
| 35 | + // 从 test-data/img 目录随机选择一张图片 | ||
| 36 | + const randomImage = getRandomImage(); | ||
| 37 | + | ||
| 38 | + await customerPage.createCustomer( | ||
| 39 | + { | ||
| 40 | + name: customerInfo.name, | ||
| 41 | + phone: customerInfo.phone, | ||
| 42 | + idCard: customerInfo.idCard, | ||
| 43 | + detailedAddress: customerInfo.detailedAddress, | ||
| 44 | + }, | ||
| 45 | + { | ||
| 46 | + creditLimit: '500', | ||
| 47 | + licensePlate: '渝ZY0706', | ||
| 48 | + province: '江苏省', | ||
| 49 | + city: '连云港市', | ||
| 50 | + district: '海州区', | ||
| 51 | + imagePath: randomImage || undefined, // 如果有图片则上传 | ||
| 52 | + } | ||
| 53 | + ); | ||
| 54 | + await customerPage.attachScreenshot(testInfo, '新增客户成功截图'); | ||
| 55 | + }); | ||
| 56 | + | ||
| 57 | + // 步骤3:验证客户创建成功 | ||
| 58 | + await allure.step('验证客户创建成功', async () => { | ||
| 59 | + const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name); | ||
| 60 | + expect(isCreated).toBeTruthy(); | ||
| 61 | + }); | ||
| 62 | + }); | ||
| 63 | + | ||
| 64 | +// test('新增客户 - 仅必填信息', async ({ customerPage }, testInfo) => { | ||
| 65 | +// // 添加allure元素 | ||
| 66 | +// await allure.epic('客户管理'); | ||
| 67 | +// await allure.feature('客户信息'); | ||
| 68 | +// await allure.story('创建新客户(仅必填信息)'); | ||
| 69 | + | ||
| 70 | +// // 步骤1:生成随机客户信息 | ||
| 71 | +// const customerInfo = await allure.step('生成随机客户信息', async (step) => { | ||
| 72 | +// const info = generateCustomerInfo(); | ||
| 73 | +// console.log('生成的客户信息:', info); | ||
| 74 | +// return info; | ||
| 75 | +// }); | ||
| 76 | + | ||
| 77 | +// // 步骤2:执行新增客户操作 | ||
| 78 | +// await allure.step('填写并提交客户表单(仅必填信息)', async () => { | ||
| 79 | +// await customerPage.gotoHome(); | ||
| 80 | +// await customerPage.createCustomer({ | ||
| 81 | +// name: customerInfo.name, | ||
| 82 | +// phone: customerInfo.phone, | ||
| 83 | +// idCard: customerInfo.idCard, | ||
| 84 | +// }); | ||
| 85 | +// await customerPage.attachScreenshot(testInfo, '新增客户成功截图'); | ||
| 86 | +// }); | ||
| 87 | + | ||
| 88 | +// // 步骤3:验证客户创建成功 | ||
| 89 | +// await allure.step('验证客户创建成功', async () => { | ||
| 90 | +// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name); | ||
| 91 | +// expect(isCreated).toBeTruthy(); | ||
| 92 | +// }); | ||
| 93 | +// }); | ||
| 94 | + | ||
| 95 | +// test('新增客户 - 带详细地址', async ({ customerPage }, testInfo) => { | ||
| 96 | +// // 添加allure元素 | ||
| 97 | +// await allure.epic('客户管理'); | ||
| 98 | +// await allure.feature('客户信息'); | ||
| 99 | +// await allure.story('创建新客户(带详细地址)'); | ||
| 100 | + | ||
| 101 | +// // 步骤1:生成随机客户信息 | ||
| 102 | +// const customerInfo = await allure.step('生成随机客户信息', async (step) => { | ||
| 103 | +// const name = generateCustomerName(); | ||
| 104 | +// const phone = generatePhoneNumber(); | ||
| 105 | +// const idCard = generateIdCard(); | ||
| 106 | +// const detailedAddress = generateDetailedAddress(); | ||
| 107 | + | ||
| 108 | +// console.log('生成的客户信息:', { name, phone, idCard, detailedAddress }); | ||
| 109 | + | ||
| 110 | +// return { name, phone, idCard, detailedAddress }; | ||
| 111 | +// }); | ||
| 112 | + | ||
| 113 | +// // 步骤2:执行新增客户操作 | ||
| 114 | +// await allure.step('填写并提交客户表单', async () => { | ||
| 115 | +// await customerPage.gotoHome(); | ||
| 116 | +// await customerPage.createCustomer( | ||
| 117 | +// { | ||
| 118 | +// name: customerInfo.name, | ||
| 119 | +// phone: customerInfo.phone, | ||
| 120 | +// idCard: customerInfo.idCard, | ||
| 121 | +// }, | ||
| 122 | +// { | ||
| 123 | +// creditLimit: '10000', | ||
| 124 | +// } | ||
| 125 | +// ); | ||
| 126 | +// await customerPage.attachScreenshot(testInfo, '新增客户成功截图'); | ||
| 127 | +// }); | ||
| 128 | + | ||
| 129 | +// // 步骤3:验证客户创建成功 | ||
| 130 | +// await allure.step('验证客户创建成功', async () => { | ||
| 131 | +// const isCreated = await customerPage.verifyCustomerCreated(customerInfo.name); | ||
| 132 | +// expect(isCreated).toBeTruthy(); | ||
| 133 | +// }); | ||
| 134 | +// }); | ||
| 135 | +}); | ||
| 0 | \ No newline at end of file | 136 | \ No newline at end of file |
utils/dataGenerator.ts
| 1 | +import * as fs from 'fs'; | ||
| 2 | +import * as path from 'path'; | ||
| 3 | + | ||
| 1 | //商品管理-随机商品名 | 4 | //商品管理-随机商品名 |
| 2 | export function generateUniqueProductName(baseName: string = '测试商品'): string { | 5 | export function generateUniqueProductName(baseName: string = '测试商品'): string { |
| 3 | // 使用时间戳,精确到毫秒,保证每次都不一样 | 6 | // 使用时间戳,精确到毫秒,保证每次都不一样 |
| @@ -14,4 +17,159 @@ export function generateOtherName(baseName: string = '批次别名'): string { | @@ -14,4 +17,159 @@ export function generateOtherName(baseName: string = '批次别名'): string { | ||
| 14 | // 再加一个随机数,防止同一毫秒内多次调用 | 17 | // 再加一个随机数,防止同一毫秒内多次调用 |
| 15 | // const random = Math.floor(Math.random() * 100); | 18 | // const random = Math.floor(Math.random() * 100); |
| 16 | return `${baseName}${timestamp}`; | 19 | return `${baseName}${timestamp}`; |
| 17 | -} | ||
| 18 | \ No newline at end of file | 20 | \ No newline at end of file |
| 21 | +} | ||
| 22 | + | ||
| 23 | +// 随机生成中文客户名称 | ||
| 24 | +export function generateCustomerName(): string { | ||
| 25 | + const familyNames = ['张', '王', '李', '刘', '陈', '杨', '黄', '赵', '周', '吴', '徐', '孙', '马', '朱', '胡', '郭', '何', '林', '罗', '高']; | ||
| 26 | + const givenNames = ['伟', '芳', '娜', '敏', '静', '丽', '强', '磊', '军', '洋', '勇', '艳', '杰', '涛', '明', '超', '秀英', '华', '平', '刚', '桂英', '文', '志强', '建华', '建国', '建军', '晓明', '小红', '建军', '婷婷', '秀兰', '伟', '芳', '军', '勇', '艳', '杰', '娟', '涛', '明', '超', '秀英', '华']; | ||
| 27 | + | ||
| 28 | + const familyName = familyNames[Math.floor(Math.random() * familyNames.length)]; | ||
| 29 | + const givenName = givenNames[Math.floor(Math.random() * givenNames.length)]; | ||
| 30 | + | ||
| 31 | + return familyName + givenName; | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +// 随机生成身份证号码(18位) | ||
| 35 | +export function generateIdCard(): string { | ||
| 36 | + // 省份代码 | ||
| 37 | + const provinceCodes = ['11', '12', '13', '14', '15', '21', '22', '23', '31', '32', '33', '34', '35', '36', '37', '41', '42', '43', '44', '45', '46', '50', '51', '52', '53', '54', '61', '62', '63', '64', '65']; | ||
| 38 | + | ||
| 39 | + // 随机选择省份 | ||
| 40 | + const provinceCode = provinceCodes[Math.floor(Math.random() * provinceCodes.length)]; | ||
| 41 | + | ||
| 42 | + // 随机生成城市和区县代码(各2位) | ||
| 43 | + const cityCode = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0'); | ||
| 44 | + const districtCode = String(Math.floor(Math.random() * 99) + 1).padStart(2, '0'); | ||
| 45 | + | ||
| 46 | + // 随机生成出生日期(1950-01-01 到 2005-12-31) | ||
| 47 | + const startYear = 1950; | ||
| 48 | + const endYear = 2005; | ||
| 49 | + const year = Math.floor(Math.random() * (endYear - startYear + 1)) + startYear; | ||
| 50 | + const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0'); | ||
| 51 | + const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0'); | ||
| 52 | + const birthDate = `${year}${month}${day}`; | ||
| 53 | + | ||
| 54 | + // 随机生成顺序码(3位) | ||
| 55 | + const sequenceCode = String(Math.floor(Math.random() * 999)).padStart(3, '0'); | ||
| 56 | + | ||
| 57 | + // 前17位 | ||
| 58 | + const id17 = provinceCode + cityCode + districtCode + birthDate + sequenceCode; | ||
| 59 | + | ||
| 60 | + // 计算校验码 | ||
| 61 | + const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; | ||
| 62 | + const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; | ||
| 63 | + | ||
| 64 | + let sum = 0; | ||
| 65 | + for (let i = 0; i < 17; i++) { | ||
| 66 | + sum += parseInt(id17[i]) * weights[i]; | ||
| 67 | + } | ||
| 68 | + const checkCode = checkCodes[sum % 11]; | ||
| 69 | + | ||
| 70 | + return id17 + checkCode; | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// 隨机生成手机号码 | ||
| 74 | +export function generatePhoneNumber(): string { | ||
| 75 | + // 中国移动号段 | ||
| 76 | + const mobilePrefixes = ['134', '135', '136', '137', '138', '139', '147', '150', '151', '152', '157', '158', '159', '178', '182', '183', '184', '187', '188']; | ||
| 77 | + // 中国联通号段 | ||
| 78 | + const unicomPrefixes = ['130', '131', '132', '145', '155', '156', '175', '176', '185', '186']; | ||
| 79 | + // 中国电信号段 | ||
| 80 | + const telecomPrefixes = ['133', '149', '153', '177', '180', '181', '189']; | ||
| 81 | + | ||
| 82 | + const allPrefixes = [...mobilePrefixes, ...unicomPrefixes, ...telecomPrefixes]; | ||
| 83 | + | ||
| 84 | + const prefix = allPrefixes[Math.floor(Math.random() * allPrefixes.length)]; | ||
| 85 | + const suffix = String(Math.floor(Math.random() * 100000000)).padStart(8, '0'); | ||
| 86 | + | ||
| 87 | + return prefix + suffix; | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +// 随机生成详细地址(街道、门牌号等,不含省市区) | ||
| 91 | +export function generateDetailedAddress(): string { | ||
| 92 | + const streets = ['中山路', '人民路', '建设路', '解放路', '和平路', '幸福路', '文化路', '科技路', '商业街', '繁华大道', '朝阳路', '胜利路', '长江路', '黄河路', '振兴路', '发展大道', '创业路', '创新路', '东风路', '西山路']; | ||
| 93 | + const communities = ['阳光小区', '幸福花园', '和谐家园', '金色年华', '锦绣华庭', '碧水蓝天', '盛世华城', '锦绣江南', '紫荆花园', '翠竹苑', '莲花小区', '牡丹园', '玫瑰园', '百合苑', '茉莉花园']; | ||
| 94 | + const buildings = ['1栋', '2栋', '3栋', '5栋', '6栋', '8栋', '9栋', '10栋', 'A座', 'B座', 'C座', 'D座']; | ||
| 95 | + const units = ['1单元', '2单元', '3单元', '一单元', '二单元', '三单元']; | ||
| 96 | + const floors = ['1楼', '2楼', '3楼', '5楼', '6楼', '8楼', '10楼', '12楼', '15楼', '18楼', '20楼']; | ||
| 97 | + const rooms = ['101室', '201室', '301室', '401室', '501室', '601室', '701室', '801室', '1001室', '1101室', '1201室']; | ||
| 98 | + | ||
| 99 | + const street = streets[Math.floor(Math.random() * streets.length)]; | ||
| 100 | + const community = communities[Math.floor(Math.random() * communities.length)]; | ||
| 101 | + const building = buildings[Math.floor(Math.random() * buildings.length)]; | ||
| 102 | + const unit = units[Math.floor(Math.random() * units.length)]; | ||
| 103 | + const floor = floors[Math.floor(Math.random() * floors.length)]; | ||
| 104 | + const room = rooms[Math.floor(Math.random() * rooms.length)]; | ||
| 105 | + | ||
| 106 | + // 随机组合地址格式 | ||
| 107 | + const formats = [ | ||
| 108 | + `${street}${Math.floor(Math.random() * 999) + 1}号`, | ||
| 109 | + `${street}${Math.floor(Math.random() * 999) + 1}号${community}`, | ||
| 110 | + `${community}${building}${unit}${room}`, | ||
| 111 | + `${street}${Math.floor(Math.random() * 999) + 1}号${community}${building}${room}`, | ||
| 112 | + `${street}${Math.floor(Math.random() * 999) + 1}号${floor}${room}`, | ||
| 113 | + `${community}${building}${unit}${floor}${room}` | ||
| 114 | + ]; | ||
| 115 | + | ||
| 116 | + return formats[Math.floor(Math.random() * formats.length)]; | ||
| 117 | +} | ||
| 118 | + | ||
| 119 | +// 生成完整的客户信息对象 | ||
| 120 | +export function generateCustomerInfo(): { | ||
| 121 | + name: string; | ||
| 122 | + idCard: string; | ||
| 123 | + phone: string; | ||
| 124 | + detailedAddress: string; | ||
| 125 | +} { | ||
| 126 | + return { | ||
| 127 | + name: generateCustomerName(), | ||
| 128 | + idCard: generateIdCard(), | ||
| 129 | + phone: generatePhoneNumber(), | ||
| 130 | + detailedAddress: generateDetailedAddress() | ||
| 131 | + }; | ||
| 132 | +} | ||
| 133 | + | ||
| 134 | +/** | ||
| 135 | + * 从指定目录随机选择一张图片 | ||
| 136 | + * @param imageDir 图片目录路径,默认为 'test-data/img' | ||
| 137 | + * @returns 随机选择的图片完整路径,如果目录不存在或没有图片则返回 null | ||
| 138 | + */ | ||
| 139 | +export function getRandomImage(imageDir: string = 'test-data/img'): string | null { | ||
| 140 | + try { | ||
| 141 | + // 获取项目根目录 | ||
| 142 | + const projectRoot = process.cwd(); | ||
| 143 | + const fullImagePath = path.join(projectRoot, imageDir); | ||
| 144 | + | ||
| 145 | + // 检查目录是否存在 | ||
| 146 | + if (!fs.existsSync(fullImagePath)) { | ||
| 147 | + console.warn(`图片目录不存在: ${fullImagePath}`); | ||
| 148 | + return null; | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + // 读取目录中的所有文件 | ||
| 152 | + const files = fs.readdirSync(fullImagePath); | ||
| 153 | + | ||
| 154 | + // 过滤出图片文件(支持常见图片格式) | ||
| 155 | + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']; | ||
| 156 | + const imageFiles = files.filter(file => { | ||
| 157 | + const ext = path.extname(file).toLowerCase(); | ||
| 158 | + return imageExtensions.includes(ext); | ||
| 159 | + }); | ||
| 160 | + | ||
| 161 | + if (imageFiles.length === 0) { | ||
| 162 | + console.warn(`图片目录中没有图片文件: ${fullImagePath}`); | ||
| 163 | + return null; | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + // 随机选择一张图片 | ||
| 167 | + const randomIndex = Math.floor(Math.random() * imageFiles.length); | ||
| 168 | + const selectedImage = imageFiles[randomIndex]; | ||
| 169 | + | ||
| 170 | + // 返回相对于项目根目录的路径 | ||
| 171 | + return path.join(imageDir, selectedImage); | ||
| 172 | + } catch (error) { | ||
| 173 | + console.error('获取随机图片失败:', error); | ||
| 174 | + return null; | ||
| 175 | + } | ||
| 176 | +} |