Commit 30d1471a6332b79044d25f612908d10af5bad231

Authored by 赵旭婷
1 parent 140ac106

添加车牌号、上传图片方法封装

pages/basePage.ts
@@ -221,4 +221,110 @@ export abstract class BasePage { @@ -221,4 +221,110 @@ export abstract class BasePage {
221 filterLocator(selector: string, options: { hasText?: string | RegExp; has?: Locator }): Locator { 221 filterLocator(selector: string, options: { hasText?: string | RegExp; has?: Locator }): Locator {
222 return this.page.locator(selector).filter(options); 222 return this.page.locator(selector).filter(options);
223 } 223 }
  224 +
  225 + /**
  226 + * 填写车牌号(通用方法)
  227 + * @param licensePlate 车牌号(如:渝ZY0706)
  228 + * @param inputLocator 车牌号输入框定位器(可选,如果不传则需要先点击输入框)
  229 + */
  230 + async fillLicensePlate(licensePlate: string, inputLocator?: Locator): Promise<void> {
  231 + // 等待页面稳定
  232 + await this.page.waitForTimeout(500);
  233 +
  234 + // 如果传入了输入框定位器,先点击它
  235 + if (inputLocator) {
  236 + await inputLocator.click();
  237 + }
  238 +
  239 + // 等待车牌键盘面板加载完成
  240 + await this.page.waitForTimeout(1000);
  241 +
  242 + // 解析车牌号的省份和字母部分
  243 + const province = licensePlate.charAt(0); // 如:渝
  244 + const letter = licensePlate.charAt(1); // 如:Z
  245 + const numbers = licensePlate.substring(2); // 如:Y0706
  246 +
  247 + // 点击省份
  248 + await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${province}$`) }).click();
  249 +
  250 + // 等待字母键盘出现
  251 + await this.page.waitForTimeout(300);
  252 +
  253 + // 点击第一个字母 - 智能选择,优先使用 nth(1),如果不存在则用 first
  254 + const letterLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${letter}$`) });
  255 + const letterCount = await letterLocator.count();
  256 + if (letterCount > 1) {
  257 + await letterLocator.nth(1).click();
  258 + } else if (letterCount === 1) {
  259 + await letterLocator.click();
  260 + } else {
  261 + // 回退到 getByText 方式
  262 + await this.page.getByText(letter, { exact: true }).first().click();
  263 + }
  264 +
  265 + // 逐个点击车牌号码
  266 + for (let i = 0; i < numbers.length; i++) {
  267 + const char = numbers[i];
  268 + await this.page.waitForTimeout(100);
  269 + if (char.match(/[A-Za-z]/)) {
  270 + // 字母
  271 + const charLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char.toUpperCase()}$`) });
  272 + const charCount = await charLocator.count();
  273 + if (charCount > 1) {
  274 + await charLocator.nth(1).click();
  275 + } else {
  276 + await charLocator.click();
  277 + }
  278 + } else if (char.match(/[0-9]/)) {
  279 + // 数字
  280 + const digitLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) });
  281 + const digitCount = await digitLocator.count();
  282 + if (digitCount > 1) {
  283 + await digitLocator.nth(1).click();
  284 + } else {
  285 + await digitLocator.click();
  286 + }
  287 + }
  288 + }
  289 +
  290 + // 点击确定按钮
  291 + await this.page.getByText('确定').click();
  292 + }
  293 +
  294 + /**
  295 + * 上传图片(通用方法)- 使用 filechooser 事件
  296 + * @param imagePath 图片文件路径(相对于项目根目录)
  297 + * @param uploadButtonLocator 上传按钮定位器(可选,默认使用 uni-scroll-view uni-button)
  298 + */
  299 + async uploadImage(imagePath: string, uploadButtonLocator?: Locator): Promise<void> {
  300 + // 使用 Playwright 的 file_chooser 事件处理文件上传
  301 + // 监听文件选择器事件
  302 + const fileChooserPromise = this.page.waitForEvent('filechooser');
  303 +
  304 + // 点击上传按钮触发文件选择器
  305 + if (uploadButtonLocator) {
  306 + await uploadButtonLocator.click();
  307 + } else {
  308 + await this.page.locator('uni-scroll-view uni-button').click();
  309 + }
  310 +
  311 + // 等待文件选择器出现并设置文件
  312 + const fileChooser = await fileChooserPromise;
  313 + await fileChooser.setFiles(imagePath);
  314 +
  315 + // 等待上传完成
  316 + await this.page.waitForTimeout(1000);
  317 + }
  318 +
  319 + /**
  320 + * 上传图片(使用 setInputFiles 方式)
  321 + * @param imagePath 图片文件路径
  322 + */
  323 + async uploadImageByInput(imagePath: string): Promise<void> {
  324 + // 直接在 body 上设置文件
  325 + await this.page.locator('body').setInputFiles(imagePath);
  326 +
  327 + // 等待上传完成
  328 + await this.page.waitForTimeout(1000);
  329 + }
224 } 330 }
pages/consignmentPage.ts
@@ -14,7 +14,10 @@ const selectors = { @@ -14,7 +14,10 @@ const selectors = {
14 warehouseSelector: '.nut-input__mask', 14 warehouseSelector: '.nut-input__mask',
15 warehouseOption: (warehouseName: string) => `uni-view:has-text("${warehouseName}")`, 15 warehouseOption: (warehouseName: string) => `uni-view:has-text("${warehouseName}")`,
16 mainTitleOption: '.mainTitle', 16 mainTitleOption: '.mainTitle',
17 - 17 + //车牌
  18 + licensePlateInput:'uni-view:nth-child(2) > .nut-cell__value > .nut-form-item__body__slots > .input-wrapper > .flex-input > .nut-input > .nut-input__value > .nut-input__mask',
  19 + confirmLicenceButton:'text=确定',
  20 +
18 // 批次别名输入 21 // 批次别名输入
19 batchAliasInput: 'uni-input:has-text("请输入批次别名") input', 22 batchAliasInput: 'uni-input:has-text("请输入批次别名") input',
20 23
@@ -67,7 +70,13 @@ export class ConsignmentPage extends BasePage { @@ -67,7 +70,13 @@ export class ConsignmentPage extends BasePage {
67 // 操作按钮 70 // 操作按钮
68 readonly createButton: Locator; 71 readonly createButton: Locator;
69 readonly saveButton: Locator; 72 readonly saveButton: Locator;
70 - readonly doneButton:Locator; 73 + readonly doneButton: Locator;
  74 +
  75 + // 车牌号
  76 + readonly licensePlateInput: Locator;
  77 +
  78 + // 图片上传
  79 + readonly uploadImageButton: Locator;
71 80
72 constructor(page: Page) { 81 constructor(page: Page) {
73 super(page); 82 super(page);
@@ -85,7 +94,11 @@ export class ConsignmentPage extends BasePage { @@ -85,7 +94,11 @@ export class ConsignmentPage extends BasePage {
85 this.paymentItem = page.locator('.payment-item'); 94 this.paymentItem = page.locator('.payment-item');
86 this.createButton = page.getByText('创建', { exact: true }); 95 this.createButton = page.getByText('创建', { exact: true });
87 this.saveButton = page.getByText('保存'); 96 this.saveButton = page.getByText('保存');
88 - this.doneButton = page.getByText('完成') 97 + this.doneButton = page.getByText('完成');
  98 + // 车牌号输入框 - nth-child(2)
  99 + this.licensePlateInput = page.locator('uni-view:nth-child(2) > .nut-cell__value > .nut-form-item__body__slots > .nut-input > .nut-input__value > .nut-input__mask');
  100 + // 图片上传区域
  101 + this.uploadImageButton = page.getByText('图片(最多上传9张)');
89 } 102 }
90 103
91 /** 104 /**
@@ -169,36 +182,36 @@ export class ConsignmentPage extends BasePage { @@ -169,36 +182,36 @@ export class ConsignmentPage extends BasePage {
169 await this.productListButton.click(); 182 await this.productListButton.click();
170 } 183 }
171 184
172 - // /**  
173 - // * 获取商品列表中所有可用的商品名称  
174 - // * @returns 商品名称数组  
175 - // */  
176 - // async getAvailableProducts(): Promise<string[]> {  
177 - // // 等待商品列表加载  
178 - // await this.page.locator('.productName').first().waitFor({ state: 'visible', timeout: 5000 }); 185 + /**
  186 + * 获取商品列表中所有可用的商品名称
  187 + * @returns 商品名称数组
  188 + */
  189 + async getAvailableProducts(): Promise<string[]> {
  190 + // 等待商品列表加载
  191 + await this.page.locator('.productName').first().waitFor({ state: 'visible', timeout: 5000 });
179 192
180 - // // 获取所有商品名称  
181 - // const productElements = await this.page.locator('.productName').allTextContents();  
182 - // return productElements.filter(name => name.trim() !== '');  
183 - // } 193 + // 获取所有商品名称
  194 + const productElements = await this.page.locator('.productName').allTextContents();
  195 + return productElements.filter(name => name.trim() !== '');
  196 + }
184 197
185 - // /**  
186 - // * 随机选择一个商品  
187 - // * @returns 随机选择的商品名称  
188 - // */  
189 - // async selectRandomProduct(): Promise<string> {  
190 - // const products = await this.getAvailableProducts(); 198 + /**
  199 + * 随机选择一个商品
  200 + * @returns 随机选择的商品名称
  201 + */
  202 + async selectRandomProduct(): Promise<string> {
  203 + const products = await this.getAvailableProducts();
191 204
192 - // if (products.length === 0) {  
193 - // throw new Error('商品列表为空,无法选择商品');  
194 - // } 205 + if (products.length === 0) {
  206 + throw new Error('商品列表为空,无法选择商品');
  207 + }
195 208
196 - // const randomIndex = Math.floor(Math.random() * products.length);  
197 - // const selectedProduct = products[randomIndex];  
198 - // console.log(`随机选择的商品: ${selectedProduct}`); 209 + const randomIndex = Math.floor(Math.random() * products.length);
  210 + const selectedProduct = products[randomIndex];
  211 + console.log(`随机选择的商品: ${selectedProduct}`);
199 212
200 - // return selectedProduct;  
201 - // } 213 + return selectedProduct;
  214 + }
202 215
203 /** 216 /**
204 * 选择商品 217 * 选择商品
@@ -229,14 +242,20 @@ export class ConsignmentPage extends BasePage { @@ -229,14 +242,20 @@ export class ConsignmentPage extends BasePage {
229 242
230 /** 243 /**
231 * 选择商品并输入数量 244 * 选择商品并输入数量
232 - * @param productName 商品名称 245 + * @param productName 商品名称(可选,不传则随机选择)
233 * @param quantity 数量 246 * @param quantity 数量
  247 + * @returns 实际选择的商品名称
234 */ 248 */
235 - async selectProductWithQuantity(productName: string, quantity: string): Promise<void> { 249 + async selectProductWithQuantity(productName?: string, quantity: string = '10'): Promise<string> {
236 await this.openProductList(); 250 await this.openProductList();
237 - await this.selectProduct(productName); 251 +
  252 + // 如果没有指定商品名称,则随机选择
  253 + const selectedProduct = productName || await this.selectRandomProduct();
  254 + await this.selectProduct(selectedProduct);
238 await this.enterProductQuantity(quantity); 255 await this.enterProductQuantity(quantity);
239 await this.completeProductSelection(); 256 await this.completeProductSelection();
  257 +
  258 + return selectedProduct;
240 } 259 }
241 260
242 /** 261 /**
@@ -297,22 +316,33 @@ export class ConsignmentPage extends BasePage { @@ -297,22 +316,33 @@ export class ConsignmentPage extends BasePage {
297 * @param batchName 批次名称 316 * @param batchName 批次名称
298 */ 317 */
299 async expectBatchCreated(batchName: string): Promise<void> { 318 async expectBatchCreated(batchName: string): Promise<void> {
300 - await expect(this.page.locator('uni-scroll-view').getByText(batchName)).toBeVisible(); 319 + await expect(this.page.locator('uni-scroll-view').getByText(batchName)).toBeVisible({ timeout: 10000 });
301 } 320 }
302 321
303 /** 322 /**
304 * 完整的代销入库创建流程 323 * 完整的代销入库创建流程
305 * @param batchAlias 批次别名 324 * @param batchAlias 批次别名
306 - * @param productName 商品名称  
307 - * @param quantity 数量  
308 - * @param amount 费用金额 325 + * @param options 可选配置
  326 + * @returns 实际选择的商品名称
309 */ 327 */
310 async createConsignmentOrder( 328 async createConsignmentOrder(
311 batchAlias: string, 329 batchAlias: string,
312 - productName: string,  
313 - quantity: string = '10',  
314 - amount: string = '1'  
315 - ): Promise<void> { 330 + options?: {
  331 + productName?: string; // 商品名称(可选,不传则随机选择)
  332 + quantity?: string; // 数量,默认 '10'
  333 + amount?: string; // 费用金额,默认 '1'
  334 + licensePlate?: string; // 车牌号(可选)
  335 + imagePath?: string; // 图片路径(可选)
  336 + }
  337 + ): Promise<string> {
  338 + const {
  339 + productName,
  340 + quantity = '10',
  341 + amount = '1',
  342 + licensePlate,
  343 + imagePath
  344 + } = options || {};
  345 +
316 // 导航到新增页面 346 // 导航到新增页面
317 await this.navigateToNewConsignment(); 347 await this.navigateToNewConsignment();
318 348
@@ -323,8 +353,23 @@ export class ConsignmentPage extends BasePage { @@ -323,8 +353,23 @@ export class ConsignmentPage extends BasePage {
323 // 输入批次别名 353 // 输入批次别名
324 await this.enterBatchAlias(batchAlias); 354 await this.enterBatchAlias(batchAlias);
325 355
326 - // 选择商品  
327 - await this.selectProductWithQuantity(productName, quantity); 356 + // 选择商品(不传productName则随机选择)
  357 + const selectedProduct = await this.selectProductWithQuantity(productName, quantity);
  358 +
  359 + // 填写车牌号(可选)
  360 + if (licensePlate) {
  361 + await this.fillLicensePlate(licensePlate, this.licensePlateInput);
  362 + }
  363 +
  364 + // 上传图片(可选)
  365 + if (imagePath) {
  366 + // 点击图片上传区域
  367 + await this.uploadImageButton.click();
  368 + // 点击上传按钮
  369 + await this.page.locator('span').filter({ hasText: '供应商*请选择供应商车牌号请选择车牌号入库日期*请选择入库日期入库仓库*请选择仓库批次别名*请输入批次别名备注请输入备注商品清单点击选择商品入库费用添加费用存储' }).locator('uni-button').click();
  370 + // 使用 setInputFiles 方式上传
  371 + await this.page.locator('body').setInputFiles(imagePath);
  372 + }
328 373
329 // 添加费用 374 // 添加费用
330 await this.addExpense(0, amount); 375 await this.addExpense(0, amount);
@@ -336,19 +381,32 @@ export class ConsignmentPage extends BasePage { @@ -336,19 +381,32 @@ export class ConsignmentPage extends BasePage {
336 381
337 // 验证 382 // 验证
338 await this.expectBatchCreated(batchAlias); 383 await this.expectBatchCreated(batchAlias);
  384 +
  385 + return selectedProduct;
339 } 386 }
340 387
341 /** 388 /**
342 * 简化的代销入库创建流程(不添加费用) 389 * 简化的代销入库创建流程(不添加费用)
343 * @param batchAlias 批次别名 390 * @param batchAlias 批次别名
344 - * @param productName 商品名称  
345 - * @param quantity 数量 391 + * @param options 可选配置
  392 + * @returns 实际选择的商品名称
346 */ 393 */
347 async createSimpleConsignmentOrder( 394 async createSimpleConsignmentOrder(
348 batchAlias: string, 395 batchAlias: string,
349 - productName: string,  
350 - quantity: string = '10'  
351 - ): Promise<void> { 396 + options?: {
  397 + productName?: string; // 商品名称(可选,不传则随机选择)
  398 + quantity?: string; // 数量,默认 '10'
  399 + licensePlate?: string; // 车牌号(可选)
  400 + imagePath?: string; // 图片路径(可选)
  401 + }
  402 + ): Promise<string> {
  403 + const {
  404 + productName,
  405 + quantity = '10',
  406 + licensePlate,
  407 + imagePath
  408 + } = options || {};
  409 +
352 // 导航到新增页面 410 // 导航到新增页面
353 await this.navigateToNewConsignment(); 411 await this.navigateToNewConsignment();
354 412
@@ -359,11 +417,23 @@ export class ConsignmentPage extends BasePage { @@ -359,11 +417,23 @@ export class ConsignmentPage extends BasePage {
359 // 输入批次别名 417 // 输入批次别名
360 await this.enterBatchAlias(batchAlias); 418 await this.enterBatchAlias(batchAlias);
361 419
362 - // 选择商品  
363 - await this.selectProductWithQuantity(productName, quantity); 420 + // 选择商品(不传productName则随机选择)
  421 + const selectedProduct = await this.selectProductWithQuantity(productName, quantity);
  422 +
  423 + // 填写车牌号(可选)
  424 + if (licensePlate) {
  425 + await this.fillLicensePlate(licensePlate, this.licensePlateInput);
  426 + }
  427 +
  428 + // 上传图片(可选)
  429 + if (imagePath) {
  430 + await this.uploadImage(imagePath, this.uploadImageButton);
  431 + }
364 432
365 // 创建 433 // 创建
366 await this.clickCreate(); 434 await this.clickCreate();
367 await this.waitForCreationComplete(); 435 await this.waitForCreationComplete();
  436 +
  437 + return selectedProduct;
368 } 438 }
369 } 439 }
370 \ No newline at end of file 440 \ No newline at end of file
pages/customerPage.ts
@@ -183,50 +183,11 @@ export class CustomerPage extends BasePage { @@ -183,50 +183,11 @@ export class CustomerPage extends BasePage {
183 * @param licensePlate 车牌号(如:渝ZY0706) 183 * @param licensePlate 车牌号(如:渝ZY0706)
184 */ 184 */
185 async fillLicensePlate(licensePlate: string): Promise<void> { 185 async fillLicensePlate(licensePlate: string): Promise<void> {
186 - // 等待页面稳定  
187 - await this.page.waitForTimeout(500);  
188 -  
189 // 点击车牌号输入框 - nth-child(11),开启赊欠额度后位置 186 // 点击车牌号输入框 - 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); 187 + const licensePlateInput = 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');
205 188
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(); 189 + // 调用基类的通用方法
  190 + await super.fillLicensePlate(licensePlate, licensePlateInput);
230 } 191 }
231 192
232 /** 193 /**
@@ -271,19 +232,8 @@ export class CustomerPage extends BasePage { @@ -271,19 +232,8 @@ export class CustomerPage extends BasePage {
271 * @param imagePath 图片文件路径(相对于项目根目录) 232 * @param imagePath 图片文件路径(相对于项目根目录)
272 */ 233 */
273 async uploadCustomerImage(imagePath: string): Promise<void> { 234 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); 235 + // 调用基类的通用方法
  236 + await super.uploadImage(imagePath);
287 } 237 }
288 238
289 /** 239 /**
tests/consignmentOrder.spec.ts
@@ -16,7 +16,7 @@ test.describe(&#39;代销入库&#39;, () =&gt; { @@ -16,7 +16,7 @@ test.describe(&#39;代销入库&#39;, () =&gt; {
16 await allure.story('创建代销入库订单'); 16 await allure.story('创建代销入库订单');
17 // 步骤1,生成批次别名 17 // 步骤1,生成批次别名
18 const batchAlias = await allure.step('生成唯一批次别名',async(step)=>{ 18 const batchAlias = await allure.step('生成唯一批次别名',async(step)=>{
19 - const alias = generateOtherName('代卖'); 19 + const alias = generateOtherName('代卖');
20 console.log('生成的批次别名:', alias); 20 console.log('生成的批次别名:', alias);
21 return alias; 21 return alias;
22 }); 22 });
@@ -24,14 +24,16 @@ test.describe(&#39;代销入库&#39;, () =&gt; { @@ -24,14 +24,16 @@ test.describe(&#39;代销入库&#39;, () =&gt; {
24 // const batchAlias = generateOtherName('代卖'); 24 // const batchAlias = generateOtherName('代卖');
25 // console.log('生成的批次别名:', batchAlias); 25 // console.log('生成的批次别名:', batchAlias);
26 26
27 - // 步骤2.执行代销入库 27 + // 步骤2.执行代销入库(商品名称不传,随机选择有库存的商品)
28 await allure.step('填写并提交代销入库表单',async()=>{ 28 await allure.step('填写并提交代销入库表单',async()=>{
29 - await consignmentPage.createConsignmentOrder(  
30 - batchAlias, // 批次别名  
31 - '娃娃菜', // 商品名称  
32 - '10', // 数量  
33 - '1' // 费用金额  
34 - ); 29 + const selectedProduct = await consignmentPage.createConsignmentOrder(batchAlias, {
  30 + quantity: '10', // 数量
  31 + amount: '1', // 费用金额
  32 + // productName 不传,随机选择
  33 + licensePlate: '渝ZY0706', // 车牌号
  34 + // imagePath: 'test-data/img/苹果.jpg' // 图片路径
  35 + });
  36 + console.log('随机选择的商品:', selectedProduct);
35 await consignmentPage.attachScreenshot(testInfo,'代销入库成功截图'); 37 await consignmentPage.attachScreenshot(testInfo,'代销入库成功截图');
36 }) 38 })
37 39