basePage.ts
10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import { Page, Locator, expect } from '@playwright/test';
import {test} from '@playwright/test';
/**
* 基础页面类 - 所有页面对象的父类
* 提供通用的页面操作方法
*/
export abstract class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
/**
截图并附加到Allure报告
@param testName 测试名称
@param screenshotName 截图名称
*/
async attachScreenshot(testInfo:any,screenshotName:string):
Promise<void>{
const screenshot = await this.page.screenshot();
await testInfo.attach(screenshotName,{
body:screenshot,
contentType:'image/png'
});
}
/**
* 导航到指定路径
* @param path 相对路径
*/
async navigate(path: string = '/'): Promise<void> {
const baseURL = process.env.BASE_URL;
if (!baseURL) {
throw new Error('BASE_URL 环境变量未设置,请设置 BASE_URL 后再运行');
}
await this.page.goto(`${baseURL}/#${path}`);
}
/**
* 等待页面加载完成
*/
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
/**
* 点击元素
* @param selector 选择器或定位器
*/
async click(selector: string | Locator): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await locator.click();
}
/**
* 点击包含指定文本的元素
* @param text 文本内容
*/
async clickByText(text: string): Promise<void> {
await this.page.getByText(text).click();
}
/**
* 填写输入框
* @param selector 选择器或定位器
* @param value 填写的值
*/
async fill(selector: string | Locator, value: string): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await locator.fill(value);
}
/**
* 清空并填写输入框
* @param selector 选择器或定位器
* @param value 填写的值
*/
async clearAndFill(selector: string | Locator, value: string): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await locator.clear();
await locator.fill(value);
}
/**
* 等待元素可见
* @param selector 选择器或定位器
* @param timeout 超时时间(毫秒)
*/
async waitForVisible(selector: string | Locator, timeout: number = 30000): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await locator.waitFor({ state: 'visible', timeout });
}
/**
* 等待元素附加到 DOM
* @param selector 选择器或定位器
* @param timeout 超时时间(毫秒)
*/
async waitForAttached(selector: string | Locator, timeout: number = 30000): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await locator.waitFor({ state: 'attached', timeout });
}
/**
* 等待元素隐藏
* @param selector 选择器或定位器
* @param timeout 超时时间(毫秒)
*/
async waitForHidden(selector: string | Locator, timeout: number = 30000): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await locator.waitFor({ state: 'hidden', timeout });
}
/**
* 断言元素可见
* @param selector 选择器或定位器
*/
async expectVisible(selector: string | Locator): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await expect(locator).toBeVisible();
}
/**
* 断言元素包含指定文本
* @param selector 选择器或定位器
* @param text 期望的文本
*/
async expectText(selector: string | Locator, text: string | RegExp): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await expect(locator).toContainText(text);
}
/**
* 断言元素具有确切文本
* @param selector 选择器或定位器
* @param text 期望的确切文本
*/
async expectExactText(selector: string | Locator, text: string | RegExp): Promise<void> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
await expect(locator).toHaveText(text);
}
/**
* 选择下拉列表选项(通过文本匹配)
* @param selector 选择器
* @param optionText 选项文本
*/
async selectOptionByText(selector: string, optionText: string): Promise<void> {
await this.page.locator(selector).selectOption({ label: optionText });
}
/**
* 获取元素文本内容
* @param selector 选择器或定位器
*/
async getText(selector: string | Locator): Promise<string | null> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
return locator.textContent();
}
/**
* 获取输入框的值
* @param selector 选择器或定位器
*/
async getInputValue(selector: string | Locator): Promise<string> {
const locator = typeof selector === 'string' ? this.page.locator(selector) : selector;
return locator.inputValue();
}
/**
* 截图
* @param name 截图名称
*/
async takeScreenshot(name: string): Promise<Buffer> {
// 使用项目根目录下的 screenshots 文件夹
const fs = require('fs');
const path = require('path');
const screenshotsDir = path.join(process.cwd(), 'screenshots');
// 确保 screenshots 目录存在
if (!fs.existsSync(screenshotsDir)) {
fs.mkdirSync(screenshotsDir, { recursive: true });
}
return this.page.screenshot({ path: path.join(screenshotsDir, `${name}.png`), fullPage: true });
}
/**
* 等待指定时间
* @param ms 毫秒数
*/
async wait(ms: number): Promise<void> {
await this.page.waitForTimeout(ms);
}
/**
* 获取页面 URL
*/
getUrl(): string {
return this.page.url();
}
/**
* 刷新页面
*/
async reload(): Promise<void> {
await this.page.reload({ waitUntil: 'networkidle' });
}
/**
* 等待导航完成
* @param timeout 超时时间
*/
async waitForNavigation(timeout: number = 30000): Promise<void> {
await this.page.waitForLoadState('load', { timeout });
}
/**
* 获取定位器
* @param selector 选择器
*/
getLocator(selector: string): Locator {
return this.page.locator(selector);
}
/**
* 过滤定位器
* @param selector 选择器
* @param options 过滤选项
*/
filterLocator(selector: string, options: { hasText?: string | RegExp; has?: Locator }): Locator {
return this.page.locator(selector).filter(options);
}
/**
* 填写车牌号(通用方法)
* @param licensePlate 车牌号(如:渝ZY0706)
* @param inputLocator 车牌号输入框定位器(可选,如果不传则需要先点击输入框)
*/
async fillLicensePlate(licensePlate: string, inputLocator?: Locator): Promise<void> {
// 等待页面稳定
await this.page.waitForTimeout(500);
// 如果传入了输入框定位器,先点击它
if (inputLocator) {
await inputLocator.click();
}
// 等待车牌键盘面板加载完成
await this.page.waitForTimeout(1000);
// 解析车牌号的省份和字母部分
const province = licensePlate.charAt(0); // 如:渝
const letter = licensePlate.charAt(1); // 如:Z
const numbers = licensePlate.substring(2); // 如:Y0706
// 点击省份
await this.page.locator('uni-view').filter({ hasText: new RegExp(`^${province}$`) }).click();
// 等待字母键盘出现
await this.page.waitForTimeout(300);
// 点击第一个字母 - 智能选择,优先使用 nth(1),如果不存在则用 first
const letterLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${letter}$`) });
const letterCount = await letterLocator.count();
if (letterCount > 1) {
await letterLocator.nth(1).click();
} else if (letterCount === 1) {
await letterLocator.click();
} else {
// 回退到 getByText 方式
await this.page.getByText(letter, { exact: true }).first().click();
}
// 逐个点击车牌号码
for (let i = 0; i < numbers.length; i++) {
const char = numbers[i];
await this.page.waitForTimeout(100);
if (char.match(/[A-Za-z]/)) {
// 字母
const charLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char.toUpperCase()}$`) });
const charCount = await charLocator.count();
if (charCount > 1) {
await charLocator.nth(1).click();
} else {
await charLocator.click();
}
} else if (char.match(/[0-9]/)) {
// 数字
const digitLocator = this.page.locator('uni-view').filter({ hasText: new RegExp(`^${char}$`) });
const digitCount = await digitLocator.count();
if (digitCount > 1) {
await digitLocator.nth(1).click();
} else {
await digitLocator.click();
}
}
}
// 点击确定按钮
await this.page.getByText('确定').click();
}
/**
* 上传图片(通用方法)- 使用 filechooser 事件
* @param imagePath 图片文件路径(相对于项目根目录)
* @param uploadButtonLocator 上传按钮定位器(可选,默认使用 uni-scroll-view uni-button)
*/
async uploadImage(imagePath: string, uploadButtonLocator?: Locator): Promise<void> {
// 使用 Playwright 的 file_chooser 事件处理文件上传
// 监听文件选择器事件
const fileChooserPromise = this.page.waitForEvent('filechooser');
// 点击上传按钮触发文件选择器
if (uploadButtonLocator) {
await uploadButtonLocator.click();
} else {
await this.page.locator('uni-scroll-view uni-button').click();
}
// 等待文件选择器出现并设置文件
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(imagePath);
// 等待上传完成
await this.page.waitForTimeout(1000);
}
/**
* 上传图片(使用 setInputFiles 方式)
* @param imagePath 图片文件路径
*/
async uploadImageByInput(imagePath: string): Promise<void> {
// 直接在 body 上设置文件
await this.page.locator('body').setInputFiles(imagePath);
// 等待上传完成
await this.page.waitForTimeout(1000);
}
}