Commit 30d1471a6332b79044d25f612908d10af5bad231

Authored by 赵旭婷
1 parent 140ac106

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

pages/basePage.ts
... ... @@ -221,4 +221,110 @@ export abstract class BasePage {
221 221 filterLocator(selector: string, options: { hasText?: string | RegExp; has?: Locator }): Locator {
222 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 14 warehouseSelector: '.nut-input__mask',
15 15 warehouseOption: (warehouseName: string) => `uni-view:has-text("${warehouseName}")`,
16 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 22 batchAliasInput: 'uni-input:has-text("请输入批次别名") input',
20 23  
... ... @@ -67,7 +70,13 @@ export class ConsignmentPage extends BasePage {
67 70 // 操作按钮
68 71 readonly createButton: Locator;
69 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 81 constructor(page: Page) {
73 82 super(page);
... ... @@ -85,7 +94,11 @@ export class ConsignmentPage extends BasePage {
85 94 this.paymentItem = page.locator('.payment-item');
86 95 this.createButton = page.getByText('创建', { exact: true });
87 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 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 242  
230 243 /**
231 244 * 选择商品并输入数量
232   - * @param productName 商品名称
  245 + * @param productName 商品名称(可选,不传则随机选择)
233 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 250 await this.openProductList();
237   - await this.selectProduct(productName);
  251 +
  252 + // 如果没有指定商品名称,则随机选择
  253 + const selectedProduct = productName || await this.selectRandomProduct();
  254 + await this.selectProduct(selectedProduct);
238 255 await this.enterProductQuantity(quantity);
239 256 await this.completeProductSelection();
  257 +
  258 + return selectedProduct;
240 259 }
241 260  
242 261 /**
... ... @@ -297,22 +316,33 @@ export class ConsignmentPage extends BasePage {
297 316 * @param batchName 批次名称
298 317 */
299 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 324 * @param batchAlias 批次别名
306   - * @param productName 商品名称
307   - * @param quantity 数量
308   - * @param amount 费用金额
  325 + * @param options 可选配置
  326 + * @returns 实际选择的商品名称
309 327 */
310 328 async createConsignmentOrder(
311 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 347 await this.navigateToNewConsignment();
318 348  
... ... @@ -323,8 +353,23 @@ export class ConsignmentPage extends BasePage {
323 353 // 输入批次别名
324 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 375 await this.addExpense(0, amount);
... ... @@ -336,19 +381,32 @@ export class ConsignmentPage extends BasePage {
336 381  
337 382 // 验证
338 383 await this.expectBatchCreated(batchAlias);
  384 +
  385 + return selectedProduct;
339 386 }
340 387  
341 388 /**
342 389 * 简化的代销入库创建流程(不添加费用)
343 390 * @param batchAlias 批次别名
344   - * @param productName 商品名称
345   - * @param quantity 数量
  391 + * @param options 可选配置
  392 + * @returns 实际选择的商品名称
346 393 */
347 394 async createSimpleConsignmentOrder(
348 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 411 await this.navigateToNewConsignment();
354 412  
... ... @@ -359,11 +417,23 @@ export class ConsignmentPage extends BasePage {
359 417 // 输入批次别名
360 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 434 await this.clickCreate();
367 435 await this.waitForCreationComplete();
  436 +
  437 + return selectedProduct;
368 438 }
369 439 }
370 440 \ No newline at end of file
... ...
pages/customerPage.ts
... ... @@ -183,50 +183,11 @@ export class CustomerPage extends BasePage {
183 183 * @param licensePlate 车牌号(如:渝ZY0706)
184 184 */
185 185 async fillLicensePlate(licensePlate: string): Promise<void> {
186   - // 等待页面稳定
187   - await this.page.waitForTimeout(500);
188   -
189 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 232 * @param imagePath 图片文件路径(相对于项目根目录)
272 233 */
273 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 16 await allure.story('创建代销入库订单');
17 17 // 步骤1,生成批次别名
18 18 const batchAlias = await allure.step('生成唯一批次别名',async(step)=>{
19   - const alias = generateOtherName('代卖');
  19 + const alias = generateOtherName('代卖');
20 20 console.log('生成的批次别名:', alias);
21 21 return alias;
22 22 });
... ... @@ -24,14 +24,16 @@ test.describe(&#39;代销入库&#39;, () =&gt; {
24 24 // const batchAlias = generateOtherName('代卖');
25 25 // console.log('生成的批次别名:', batchAlias);
26 26  
27   - // 步骤2.执行代销入库
  27 + // 步骤2.执行代销入库(商品名称不传,随机选择有库存的商品)
28 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 37 await consignmentPage.attachScreenshot(testInfo,'代销入库成功截图');
36 38 })
37 39  
... ...