kdtest-pw 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kdtest_pw/__init__.py +50 -0
- kdtest_pw/action/__init__.py +7 -0
- kdtest_pw/action/base_keyword.py +292 -0
- kdtest_pw/action/element_plus/__init__.py +23 -0
- kdtest_pw/action/element_plus/el_cascader.py +263 -0
- kdtest_pw/action/element_plus/el_datepicker.py +324 -0
- kdtest_pw/action/element_plus/el_dialog.py +317 -0
- kdtest_pw/action/element_plus/el_form.py +443 -0
- kdtest_pw/action/element_plus/el_menu.py +456 -0
- kdtest_pw/action/element_plus/el_select.py +268 -0
- kdtest_pw/action/element_plus/el_table.py +442 -0
- kdtest_pw/action/element_plus/el_tree.py +364 -0
- kdtest_pw/action/element_plus/el_upload.py +313 -0
- kdtest_pw/action/key_retrieval.py +311 -0
- kdtest_pw/action/page_action.py +1129 -0
- kdtest_pw/api/__init__.py +6 -0
- kdtest_pw/api/api_keyword.py +251 -0
- kdtest_pw/api/request_handler.py +232 -0
- kdtest_pw/cases/__init__.py +6 -0
- kdtest_pw/cases/case_collector.py +182 -0
- kdtest_pw/cases/case_executor.py +359 -0
- kdtest_pw/cases/read/__init__.py +6 -0
- kdtest_pw/cases/read/cell_handler.py +305 -0
- kdtest_pw/cases/read/excel_reader.py +223 -0
- kdtest_pw/cli/__init__.py +5 -0
- kdtest_pw/cli/run.py +318 -0
- kdtest_pw/common.py +106 -0
- kdtest_pw/core/__init__.py +7 -0
- kdtest_pw/core/browser_manager.py +196 -0
- kdtest_pw/core/config_loader.py +235 -0
- kdtest_pw/core/page_context.py +228 -0
- kdtest_pw/data/__init__.py +5 -0
- kdtest_pw/data/init_data.py +105 -0
- kdtest_pw/data/static/elementData.yaml +59 -0
- kdtest_pw/data/static/parameters.json +24 -0
- kdtest_pw/plugins/__init__.py +6 -0
- kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
- kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
- kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
- kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
- kdtest_pw/plugins/plugin_base.py +180 -0
- kdtest_pw/plugins/plugin_loader.py +260 -0
- kdtest_pw/product.py +5 -0
- kdtest_pw/reference.py +99 -0
- kdtest_pw/utils/__init__.py +13 -0
- kdtest_pw/utils/built_in_function.py +376 -0
- kdtest_pw/utils/decorator.py +211 -0
- kdtest_pw/utils/log/__init__.py +6 -0
- kdtest_pw/utils/log/html_report.py +336 -0
- kdtest_pw/utils/log/logger.py +123 -0
- kdtest_pw/utils/public_script.py +366 -0
- kdtest_pw-2.0.0.dist-info/METADATA +169 -0
- kdtest_pw-2.0.0.dist-info/RECORD +57 -0
- kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
- kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
- kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
- kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
"""核心关键字实现 - KeyWordTest 类"""
|
|
2
|
+
|
|
3
|
+
from playwright.sync_api import Page, Locator, expect
|
|
4
|
+
from typing import Optional, Union, List
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from .base_keyword import BaseKeyword
|
|
9
|
+
from ..common import KEY_MAP
|
|
10
|
+
from ..reference import GSTORE, PRIVATEDATA, INFO, set_element_value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KeyWordTest(BaseKeyword):
|
|
14
|
+
"""核心关键字实现类
|
|
15
|
+
|
|
16
|
+
兼容原 kdtest API,提供所有基础关键字操作。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, page: Page):
|
|
20
|
+
"""初始化关键字测试类
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
page: Playwright Page 实例
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(page)
|
|
26
|
+
|
|
27
|
+
# ==================== 浏览器操作 ====================
|
|
28
|
+
|
|
29
|
+
def driver_get(self, *, content: str) -> None:
|
|
30
|
+
"""导航到 URL
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
content: 目标 URL
|
|
34
|
+
"""
|
|
35
|
+
INFO(f"导航到: {content}")
|
|
36
|
+
self.page.goto(content, wait_until='domcontentloaded')
|
|
37
|
+
self.wait_for_element_plus_loading()
|
|
38
|
+
|
|
39
|
+
def driver_back(self) -> None:
|
|
40
|
+
"""浏览器后退"""
|
|
41
|
+
INFO("浏览器后退")
|
|
42
|
+
self.page.go_back()
|
|
43
|
+
|
|
44
|
+
def driver_forward(self) -> None:
|
|
45
|
+
"""浏览器前进"""
|
|
46
|
+
INFO("浏览器前进")
|
|
47
|
+
self.page.go_forward()
|
|
48
|
+
|
|
49
|
+
def driver_refresh(self) -> None:
|
|
50
|
+
"""刷新页面"""
|
|
51
|
+
INFO("刷新页面")
|
|
52
|
+
self.page.reload()
|
|
53
|
+
self.wait_for_element_plus_loading()
|
|
54
|
+
|
|
55
|
+
def driver_close(self) -> None:
|
|
56
|
+
"""关闭当前页面"""
|
|
57
|
+
INFO("关闭当前页面")
|
|
58
|
+
self.page.close()
|
|
59
|
+
|
|
60
|
+
def driver_maximize(self) -> None:
|
|
61
|
+
"""最大化窗口(设置视口为屏幕大小)"""
|
|
62
|
+
INFO("最大化窗口")
|
|
63
|
+
# Playwright 使用视口而非窗口,设置一个较大的尺寸
|
|
64
|
+
self.page.set_viewport_size({'width': 1920, 'height': 1080})
|
|
65
|
+
|
|
66
|
+
# ==================== 输入操作 ====================
|
|
67
|
+
|
|
68
|
+
def input_text(
|
|
69
|
+
self,
|
|
70
|
+
targeting: str,
|
|
71
|
+
element: str,
|
|
72
|
+
index: Optional[int] = None,
|
|
73
|
+
parent: Optional[Locator] = None,
|
|
74
|
+
*,
|
|
75
|
+
content: str
|
|
76
|
+
) -> None:
|
|
77
|
+
"""输入文本(自动清除后输入)
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
targeting: 定位类型
|
|
81
|
+
element: 定位表达式
|
|
82
|
+
index: 索引
|
|
83
|
+
parent: 父元素定位器
|
|
84
|
+
content: 输入内容
|
|
85
|
+
"""
|
|
86
|
+
INFO(f"输入文本: {content}")
|
|
87
|
+
loc = self.locator(targeting, element, index, parent)
|
|
88
|
+
loc.fill(str(content))
|
|
89
|
+
|
|
90
|
+
def input_append(
|
|
91
|
+
self,
|
|
92
|
+
targeting: str,
|
|
93
|
+
element: str,
|
|
94
|
+
index: Optional[int] = None,
|
|
95
|
+
parent: Optional[Locator] = None,
|
|
96
|
+
*,
|
|
97
|
+
content: str
|
|
98
|
+
) -> None:
|
|
99
|
+
"""追加输入文本
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
targeting: 定位类型
|
|
103
|
+
element: 定位表达式
|
|
104
|
+
index: 索引
|
|
105
|
+
parent: 父元素定位器
|
|
106
|
+
content: 追加内容
|
|
107
|
+
"""
|
|
108
|
+
INFO(f"追加输入: {content}")
|
|
109
|
+
loc = self.locator(targeting, element, index, parent)
|
|
110
|
+
loc.press_sequentially(str(content))
|
|
111
|
+
|
|
112
|
+
def input_clear(
|
|
113
|
+
self,
|
|
114
|
+
targeting: str,
|
|
115
|
+
element: str,
|
|
116
|
+
index: Optional[int] = None,
|
|
117
|
+
parent: Optional[Locator] = None
|
|
118
|
+
) -> None:
|
|
119
|
+
"""清除输入框内容
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
targeting: 定位类型
|
|
123
|
+
element: 定位表达式
|
|
124
|
+
index: 索引
|
|
125
|
+
parent: 父元素定位器
|
|
126
|
+
"""
|
|
127
|
+
INFO("清除输入框")
|
|
128
|
+
loc = self.locator(targeting, element, index, parent)
|
|
129
|
+
loc.clear()
|
|
130
|
+
|
|
131
|
+
# ==================== 点击操作 ====================
|
|
132
|
+
|
|
133
|
+
def click_btn(
|
|
134
|
+
self,
|
|
135
|
+
targeting: str,
|
|
136
|
+
element: str,
|
|
137
|
+
index: Optional[int] = None,
|
|
138
|
+
parent: Optional[Locator] = None,
|
|
139
|
+
*,
|
|
140
|
+
force: bool = False,
|
|
141
|
+
js_click: bool = False,
|
|
142
|
+
content: str = None
|
|
143
|
+
) -> None:
|
|
144
|
+
"""点击元素
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
targeting: 定位类型
|
|
148
|
+
element: 定位表达式
|
|
149
|
+
index: 索引
|
|
150
|
+
parent: 父元素定位器
|
|
151
|
+
force: 是否强制点击
|
|
152
|
+
js_click: 是否使用 JS 点击
|
|
153
|
+
content: 预留参数(兼容性)
|
|
154
|
+
"""
|
|
155
|
+
INFO(f"点击元素: {element}")
|
|
156
|
+
loc = self.locator(targeting, element, index, parent)
|
|
157
|
+
|
|
158
|
+
if js_click or content == 'js_click':
|
|
159
|
+
loc.evaluate('el => el.click()')
|
|
160
|
+
else:
|
|
161
|
+
loc.click(force=force)
|
|
162
|
+
|
|
163
|
+
def double_click(
|
|
164
|
+
self,
|
|
165
|
+
targeting: str,
|
|
166
|
+
element: str,
|
|
167
|
+
index: Optional[int] = None,
|
|
168
|
+
parent: Optional[Locator] = None
|
|
169
|
+
) -> None:
|
|
170
|
+
"""双击元素
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
targeting: 定位类型
|
|
174
|
+
element: 定位表达式
|
|
175
|
+
index: 索引
|
|
176
|
+
parent: 父元素定位器
|
|
177
|
+
"""
|
|
178
|
+
INFO(f"双击元素: {element}")
|
|
179
|
+
loc = self.locator(targeting, element, index, parent)
|
|
180
|
+
loc.dblclick()
|
|
181
|
+
|
|
182
|
+
def right_click(
|
|
183
|
+
self,
|
|
184
|
+
targeting: str,
|
|
185
|
+
element: str,
|
|
186
|
+
index: Optional[int] = None,
|
|
187
|
+
parent: Optional[Locator] = None
|
|
188
|
+
) -> None:
|
|
189
|
+
"""右键点击
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
targeting: 定位类型
|
|
193
|
+
element: 定位表达式
|
|
194
|
+
index: 索引
|
|
195
|
+
parent: 父元素定位器
|
|
196
|
+
"""
|
|
197
|
+
INFO(f"右键点击: {element}")
|
|
198
|
+
loc = self.locator(targeting, element, index, parent)
|
|
199
|
+
loc.click(button='right')
|
|
200
|
+
|
|
201
|
+
def click_and_hold(
|
|
202
|
+
self,
|
|
203
|
+
targeting: str,
|
|
204
|
+
element: str,
|
|
205
|
+
index: Optional[int] = None,
|
|
206
|
+
parent: Optional[Locator] = None
|
|
207
|
+
) -> None:
|
|
208
|
+
"""点击并按住
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
targeting: 定位类型
|
|
212
|
+
element: 定位表达式
|
|
213
|
+
index: 索引
|
|
214
|
+
parent: 父元素定位器
|
|
215
|
+
"""
|
|
216
|
+
INFO(f"点击并按住: {element}")
|
|
217
|
+
loc = self.locator(targeting, element, index, parent)
|
|
218
|
+
box = loc.bounding_box()
|
|
219
|
+
if box:
|
|
220
|
+
self.page.mouse.move(box['x'] + box['width'] / 2, box['y'] + box['height'] / 2)
|
|
221
|
+
self.page.mouse.down()
|
|
222
|
+
|
|
223
|
+
def release_click(self) -> None:
|
|
224
|
+
"""释放点击"""
|
|
225
|
+
INFO("释放点击")
|
|
226
|
+
self.page.mouse.up()
|
|
227
|
+
|
|
228
|
+
# ==================== 鼠标操作 ====================
|
|
229
|
+
|
|
230
|
+
def hover(
|
|
231
|
+
self,
|
|
232
|
+
targeting: str,
|
|
233
|
+
element: str,
|
|
234
|
+
index: Optional[int] = None,
|
|
235
|
+
parent: Optional[Locator] = None
|
|
236
|
+
) -> None:
|
|
237
|
+
"""鼠标悬停
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
targeting: 定位类型
|
|
241
|
+
element: 定位表达式
|
|
242
|
+
index: 索引
|
|
243
|
+
parent: 父元素定位器
|
|
244
|
+
"""
|
|
245
|
+
INFO(f"悬停: {element}")
|
|
246
|
+
loc = self.locator(targeting, element, index, parent)
|
|
247
|
+
loc.hover()
|
|
248
|
+
|
|
249
|
+
def drag_and_drop(
|
|
250
|
+
self,
|
|
251
|
+
source_targeting: str,
|
|
252
|
+
source_element: str,
|
|
253
|
+
target_targeting: str,
|
|
254
|
+
target_element: str,
|
|
255
|
+
*,
|
|
256
|
+
content: str = None
|
|
257
|
+
) -> None:
|
|
258
|
+
"""拖拽元素
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
source_targeting: 源元素定位类型
|
|
262
|
+
source_element: 源元素定位表达式
|
|
263
|
+
target_targeting: 目标元素定位类型
|
|
264
|
+
target_element: 目标元素定位表达式
|
|
265
|
+
content: 预留参数
|
|
266
|
+
"""
|
|
267
|
+
INFO(f"拖拽: {source_element} -> {target_element}")
|
|
268
|
+
source = self.locator(source_targeting, source_element)
|
|
269
|
+
target = self.locator(target_targeting, target_element)
|
|
270
|
+
source.drag_to(target)
|
|
271
|
+
|
|
272
|
+
def scroll_to_element(
|
|
273
|
+
self,
|
|
274
|
+
targeting: str,
|
|
275
|
+
element: str,
|
|
276
|
+
index: Optional[int] = None,
|
|
277
|
+
parent: Optional[Locator] = None
|
|
278
|
+
) -> None:
|
|
279
|
+
"""滚动到元素位置
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
targeting: 定位类型
|
|
283
|
+
element: 定位表达式
|
|
284
|
+
index: 索引
|
|
285
|
+
parent: 父元素定位器
|
|
286
|
+
"""
|
|
287
|
+
INFO(f"滚动到元素: {element}")
|
|
288
|
+
loc = self.locator(targeting, element, index, parent)
|
|
289
|
+
loc.scroll_into_view_if_needed()
|
|
290
|
+
|
|
291
|
+
def scroll_by(self, *, content: str) -> None:
|
|
292
|
+
"""滚动页面
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
content: 滚动距离,格式 "x,y" 或 "y"
|
|
296
|
+
"""
|
|
297
|
+
parts = str(content).split(',')
|
|
298
|
+
if len(parts) == 2:
|
|
299
|
+
x, y = int(parts[0]), int(parts[1])
|
|
300
|
+
else:
|
|
301
|
+
x, y = 0, int(parts[0])
|
|
302
|
+
|
|
303
|
+
INFO(f"滚动页面: ({x}, {y})")
|
|
304
|
+
self.page.mouse.wheel(x, y)
|
|
305
|
+
|
|
306
|
+
# ==================== 键盘操作 ====================
|
|
307
|
+
|
|
308
|
+
def keyboard_events(
|
|
309
|
+
self,
|
|
310
|
+
targeting: str,
|
|
311
|
+
element: str,
|
|
312
|
+
index: Optional[int] = None,
|
|
313
|
+
parent: Optional[Locator] = None,
|
|
314
|
+
*,
|
|
315
|
+
content: str
|
|
316
|
+
) -> None:
|
|
317
|
+
"""键盘事件
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
targeting: 定位类型
|
|
321
|
+
element: 定位表达式
|
|
322
|
+
index: 索引
|
|
323
|
+
parent: 父元素定位器
|
|
324
|
+
content: 按键 (如 Keys.ENTER, Keys.TAB)
|
|
325
|
+
"""
|
|
326
|
+
INFO(f"键盘事件: {content}")
|
|
327
|
+
loc = self.locator(targeting, element, index, parent)
|
|
328
|
+
|
|
329
|
+
# 转换按键
|
|
330
|
+
key = KEY_MAP.get(content, content)
|
|
331
|
+
loc.press(key)
|
|
332
|
+
|
|
333
|
+
def press_key(self, *, content: str) -> None:
|
|
334
|
+
"""全局按键
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
content: 按键
|
|
338
|
+
"""
|
|
339
|
+
INFO(f"按键: {content}")
|
|
340
|
+
key = KEY_MAP.get(content, content)
|
|
341
|
+
self.page.keyboard.press(key)
|
|
342
|
+
|
|
343
|
+
def type_text(self, *, content: str) -> None:
|
|
344
|
+
"""全局键盘输入
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
content: 输入内容
|
|
348
|
+
"""
|
|
349
|
+
INFO(f"键盘输入: {content}")
|
|
350
|
+
self.page.keyboard.type(str(content))
|
|
351
|
+
|
|
352
|
+
# ==================== 下拉选择(原生 select) ====================
|
|
353
|
+
|
|
354
|
+
def selector_operation(
|
|
355
|
+
self,
|
|
356
|
+
targeting: str,
|
|
357
|
+
element: str,
|
|
358
|
+
index: Optional[int] = None,
|
|
359
|
+
parent: Optional[Locator] = None,
|
|
360
|
+
*,
|
|
361
|
+
content: str,
|
|
362
|
+
by: str = 'label'
|
|
363
|
+
) -> None:
|
|
364
|
+
"""原生 select 下拉框选择
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
targeting: 定位类型
|
|
368
|
+
element: 定位表达式
|
|
369
|
+
index: 索引
|
|
370
|
+
parent: 父元素定位器
|
|
371
|
+
content: 选项值
|
|
372
|
+
by: 选择方式 (label, value, index)
|
|
373
|
+
"""
|
|
374
|
+
INFO(f"选择下拉框: {content}")
|
|
375
|
+
loc = self.locator(targeting, element, index, parent)
|
|
376
|
+
|
|
377
|
+
if by == 'label':
|
|
378
|
+
loc.select_option(label=content)
|
|
379
|
+
elif by == 'value':
|
|
380
|
+
loc.select_option(value=content)
|
|
381
|
+
elif by == 'index':
|
|
382
|
+
loc.select_option(index=int(content))
|
|
383
|
+
else:
|
|
384
|
+
loc.select_option(label=content)
|
|
385
|
+
|
|
386
|
+
# ==================== 复选框/单选框 ====================
|
|
387
|
+
|
|
388
|
+
def checkbox_operation(
|
|
389
|
+
self,
|
|
390
|
+
targeting: str,
|
|
391
|
+
element: str,
|
|
392
|
+
index: Optional[int] = None,
|
|
393
|
+
parent: Optional[Locator] = None,
|
|
394
|
+
*,
|
|
395
|
+
content: str = 'true'
|
|
396
|
+
) -> None:
|
|
397
|
+
"""复选框操作
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
targeting: 定位类型
|
|
401
|
+
element: 定位表达式
|
|
402
|
+
index: 索引
|
|
403
|
+
parent: 父元素定位器
|
|
404
|
+
content: 是否选中 (true/false)
|
|
405
|
+
"""
|
|
406
|
+
checked = str(content).lower() in ('true', '1', 'yes', 'on')
|
|
407
|
+
INFO(f"复选框: {'选中' if checked else '取消'}")
|
|
408
|
+
loc = self.locator(targeting, element, index, parent)
|
|
409
|
+
loc.set_checked(checked)
|
|
410
|
+
|
|
411
|
+
def radio_select(
|
|
412
|
+
self,
|
|
413
|
+
targeting: str,
|
|
414
|
+
element: str,
|
|
415
|
+
index: Optional[int] = None,
|
|
416
|
+
parent: Optional[Locator] = None
|
|
417
|
+
) -> None:
|
|
418
|
+
"""单选框选择
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
targeting: 定位类型
|
|
422
|
+
element: 定位表达式
|
|
423
|
+
index: 索引
|
|
424
|
+
parent: 父元素定位器
|
|
425
|
+
"""
|
|
426
|
+
INFO(f"选择单选框: {element}")
|
|
427
|
+
loc = self.locator(targeting, element, index, parent)
|
|
428
|
+
loc.check()
|
|
429
|
+
|
|
430
|
+
# ==================== 文件上传 ====================
|
|
431
|
+
|
|
432
|
+
def file_upload(
|
|
433
|
+
self,
|
|
434
|
+
targeting: str,
|
|
435
|
+
element: str,
|
|
436
|
+
index: Optional[int] = None,
|
|
437
|
+
parent: Optional[Locator] = None,
|
|
438
|
+
*,
|
|
439
|
+
content: str
|
|
440
|
+
) -> None:
|
|
441
|
+
"""文件上传
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
targeting: 定位类型
|
|
445
|
+
element: 定位表达式
|
|
446
|
+
index: 索引
|
|
447
|
+
parent: 父元素定位器
|
|
448
|
+
content: 文件路径(多个文件用逗号分隔)
|
|
449
|
+
"""
|
|
450
|
+
INFO(f"上传文件: {content}")
|
|
451
|
+
loc = self.locator(targeting, element, index, parent)
|
|
452
|
+
|
|
453
|
+
# 支持多文件
|
|
454
|
+
files = [f.strip() for f in str(content).split(',')]
|
|
455
|
+
if len(files) == 1:
|
|
456
|
+
loc.set_input_files(files[0])
|
|
457
|
+
else:
|
|
458
|
+
loc.set_input_files(files)
|
|
459
|
+
|
|
460
|
+
# ==================== iframe 操作 ====================
|
|
461
|
+
|
|
462
|
+
def switch_to_frame(
|
|
463
|
+
self,
|
|
464
|
+
targeting: str,
|
|
465
|
+
element: str,
|
|
466
|
+
index: Optional[int] = None,
|
|
467
|
+
parent: Optional[Locator] = None
|
|
468
|
+
) -> None:
|
|
469
|
+
"""切换到 iframe
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
targeting: 定位类型
|
|
473
|
+
element: 定位表达式
|
|
474
|
+
index: 索引
|
|
475
|
+
parent: 父元素定位器
|
|
476
|
+
"""
|
|
477
|
+
self.switch_frame(targeting, element)
|
|
478
|
+
|
|
479
|
+
def switch_to_default(self) -> None:
|
|
480
|
+
"""切换到主文档"""
|
|
481
|
+
self.frame_default()
|
|
482
|
+
|
|
483
|
+
def switch_to_parent_frame(self) -> None:
|
|
484
|
+
"""切换到父 frame"""
|
|
485
|
+
self.switch_frame_parent()
|
|
486
|
+
|
|
487
|
+
# ==================== 窗口/标签页操作 ====================
|
|
488
|
+
|
|
489
|
+
def get_handle(self, *, content: str = None) -> str:
|
|
490
|
+
"""获取当前页面标识并缓存
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
content: 缓存键名
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
str: 页面 URL
|
|
497
|
+
"""
|
|
498
|
+
url = self.page.url
|
|
499
|
+
PRIVATEDATA['PAGES'].append(self.page)
|
|
500
|
+
if content:
|
|
501
|
+
set_element_value(content, url)
|
|
502
|
+
INFO(f"页面句柄: {url}")
|
|
503
|
+
return url
|
|
504
|
+
|
|
505
|
+
def switch_to_new_page(self, *, content: str = None) -> None:
|
|
506
|
+
"""切换到新打开的页面
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
content: 预留参数
|
|
510
|
+
"""
|
|
511
|
+
INFO("切换到新页面")
|
|
512
|
+
pages = self.page.context.pages
|
|
513
|
+
if len(pages) > 1:
|
|
514
|
+
new_page = pages[-1]
|
|
515
|
+
GSTORE['page'] = new_page
|
|
516
|
+
self.page = new_page
|
|
517
|
+
|
|
518
|
+
def switch_to_page(self, *, content: str) -> None:
|
|
519
|
+
"""切换到指定索引的页面
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
content: 页面索引
|
|
523
|
+
"""
|
|
524
|
+
index = int(content)
|
|
525
|
+
INFO(f"切换到页面: {index}")
|
|
526
|
+
pages = self.page.context.pages
|
|
527
|
+
if 0 <= index < len(pages):
|
|
528
|
+
self.page = pages[index]
|
|
529
|
+
GSTORE['page'] = self.page
|
|
530
|
+
|
|
531
|
+
def switch_to_page_by_title(self, *, content: str) -> None:
|
|
532
|
+
"""根据标题切换页面
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
content: 标题模式
|
|
536
|
+
"""
|
|
537
|
+
INFO(f"切换到标题包含: {content}")
|
|
538
|
+
pages = self.page.context.pages
|
|
539
|
+
for page in pages:
|
|
540
|
+
if content in page.title():
|
|
541
|
+
self.page = page
|
|
542
|
+
GSTORE['page'] = self.page
|
|
543
|
+
return
|
|
544
|
+
INFO(f"未找到标题包含 '{content}' 的页面", "WARNING")
|
|
545
|
+
|
|
546
|
+
def switch_to_page_by_url(self, *, content: str) -> None:
|
|
547
|
+
"""根据 URL 切换页面
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
content: URL 模式
|
|
551
|
+
"""
|
|
552
|
+
INFO(f"切换到 URL 包含: {content}")
|
|
553
|
+
pages = self.page.context.pages
|
|
554
|
+
for page in pages:
|
|
555
|
+
if content in page.url:
|
|
556
|
+
self.page = page
|
|
557
|
+
GSTORE['page'] = self.page
|
|
558
|
+
return
|
|
559
|
+
INFO(f"未找到 URL 包含 '{content}' 的页面", "WARNING")
|
|
560
|
+
|
|
561
|
+
def close_other_pages(self) -> None:
|
|
562
|
+
"""关闭其他页面"""
|
|
563
|
+
INFO("关闭其他页面")
|
|
564
|
+
current = self.page
|
|
565
|
+
pages = self.page.context.pages
|
|
566
|
+
for page in pages:
|
|
567
|
+
if page != current:
|
|
568
|
+
page.close()
|
|
569
|
+
|
|
570
|
+
# ==================== 弹窗处理 ====================
|
|
571
|
+
|
|
572
|
+
def alert_accept(self) -> None:
|
|
573
|
+
"""接受弹窗"""
|
|
574
|
+
INFO("接受弹窗")
|
|
575
|
+
self.page.on('dialog', lambda dialog: dialog.accept())
|
|
576
|
+
|
|
577
|
+
def alert_dismiss(self) -> None:
|
|
578
|
+
"""取消弹窗"""
|
|
579
|
+
INFO("取消弹窗")
|
|
580
|
+
self.page.on('dialog', lambda dialog: dialog.dismiss())
|
|
581
|
+
|
|
582
|
+
def alert_text(self, *, content: str = None) -> str:
|
|
583
|
+
"""获取弹窗文本
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
content: 缓存键名
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
str: 弹窗文本
|
|
590
|
+
"""
|
|
591
|
+
text = ''
|
|
592
|
+
|
|
593
|
+
def handle_dialog(dialog):
|
|
594
|
+
nonlocal text
|
|
595
|
+
text = dialog.message
|
|
596
|
+
dialog.accept()
|
|
597
|
+
|
|
598
|
+
self.page.on('dialog', handle_dialog)
|
|
599
|
+
if content:
|
|
600
|
+
set_element_value(content, text)
|
|
601
|
+
return text
|
|
602
|
+
|
|
603
|
+
def alert_input(self, *, content: str) -> None:
|
|
604
|
+
"""prompt 弹窗输入
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
content: 输入内容
|
|
608
|
+
"""
|
|
609
|
+
INFO(f"弹窗输入: {content}")
|
|
610
|
+
self.page.on('dialog', lambda dialog: dialog.accept(content))
|
|
611
|
+
|
|
612
|
+
# ==================== 断言关键字 ====================
|
|
613
|
+
|
|
614
|
+
def title_assert(self, *, content: str, mode: str = 'equals') -> bool:
|
|
615
|
+
"""页面标题断言
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
content: 期望值
|
|
619
|
+
mode: 断言模式 (equals, contains, regex)
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
bool: 断言结果
|
|
623
|
+
"""
|
|
624
|
+
title = self.page.title()
|
|
625
|
+
result = self._assert_text(title, content, mode)
|
|
626
|
+
INFO(f"标题断言: {title} {mode} {content} -> {result}")
|
|
627
|
+
return result
|
|
628
|
+
|
|
629
|
+
def url_assert(self, *, content: str, mode: str = 'equals') -> bool:
|
|
630
|
+
"""页面 URL 断言
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
content: 期望值
|
|
634
|
+
mode: 断言模式
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
bool: 断言结果
|
|
638
|
+
"""
|
|
639
|
+
url = self.page.url
|
|
640
|
+
result = self._assert_text(url, content, mode)
|
|
641
|
+
INFO(f"URL 断言: {url} {mode} {content} -> {result}")
|
|
642
|
+
return result
|
|
643
|
+
|
|
644
|
+
def text_assert(
|
|
645
|
+
self,
|
|
646
|
+
targeting: str,
|
|
647
|
+
element: str,
|
|
648
|
+
index: Optional[int] = None,
|
|
649
|
+
parent: Optional[Locator] = None,
|
|
650
|
+
*,
|
|
651
|
+
content: str,
|
|
652
|
+
mode: str = 'equals'
|
|
653
|
+
) -> bool:
|
|
654
|
+
"""元素文本断言
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
targeting: 定位类型
|
|
658
|
+
element: 定位表达式
|
|
659
|
+
index: 索引
|
|
660
|
+
parent: 父元素定位器
|
|
661
|
+
content: 期望值
|
|
662
|
+
mode: 断言模式
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
bool: 断言结果
|
|
666
|
+
"""
|
|
667
|
+
loc = self.locator(targeting, element, index, parent)
|
|
668
|
+
actual = loc.text_content() or ''
|
|
669
|
+
actual = actual.strip()
|
|
670
|
+
result = self._assert_text(actual, content, mode)
|
|
671
|
+
INFO(f"文本断言: '{actual}' {mode} '{content}' -> {result}")
|
|
672
|
+
return result
|
|
673
|
+
|
|
674
|
+
def value_assert(
|
|
675
|
+
self,
|
|
676
|
+
targeting: str,
|
|
677
|
+
element: str,
|
|
678
|
+
index: Optional[int] = None,
|
|
679
|
+
parent: Optional[Locator] = None,
|
|
680
|
+
*,
|
|
681
|
+
content: str,
|
|
682
|
+
mode: str = 'equals'
|
|
683
|
+
) -> bool:
|
|
684
|
+
"""元素 value 属性断言
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
targeting: 定位类型
|
|
688
|
+
element: 定位表达式
|
|
689
|
+
index: 索引
|
|
690
|
+
parent: 父元素定位器
|
|
691
|
+
content: 期望值
|
|
692
|
+
mode: 断言模式
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
bool: 断言结果
|
|
696
|
+
"""
|
|
697
|
+
loc = self.locator(targeting, element, index, parent)
|
|
698
|
+
actual = loc.input_value() or ''
|
|
699
|
+
result = self._assert_text(actual, content, mode)
|
|
700
|
+
INFO(f"Value 断言: '{actual}' {mode} '{content}' -> {result}")
|
|
701
|
+
return result
|
|
702
|
+
|
|
703
|
+
def attribute_assert(
|
|
704
|
+
self,
|
|
705
|
+
targeting: str,
|
|
706
|
+
element: str,
|
|
707
|
+
index: Optional[int] = None,
|
|
708
|
+
parent: Optional[Locator] = None,
|
|
709
|
+
*,
|
|
710
|
+
attribute: str,
|
|
711
|
+
content: str,
|
|
712
|
+
mode: str = 'equals'
|
|
713
|
+
) -> bool:
|
|
714
|
+
"""元素属性断言
|
|
715
|
+
|
|
716
|
+
Args:
|
|
717
|
+
targeting: 定位类型
|
|
718
|
+
element: 定位表达式
|
|
719
|
+
index: 索引
|
|
720
|
+
parent: 父元素定位器
|
|
721
|
+
attribute: 属性名
|
|
722
|
+
content: 期望值
|
|
723
|
+
mode: 断言模式
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
bool: 断言结果
|
|
727
|
+
"""
|
|
728
|
+
loc = self.locator(targeting, element, index, parent)
|
|
729
|
+
actual = loc.get_attribute(attribute) or ''
|
|
730
|
+
result = self._assert_text(actual, content, mode)
|
|
731
|
+
INFO(f"属性断言: {attribute}='{actual}' {mode} '{content}' -> {result}")
|
|
732
|
+
return result
|
|
733
|
+
|
|
734
|
+
def element_visible_assert(
|
|
735
|
+
self,
|
|
736
|
+
targeting: str,
|
|
737
|
+
element: str,
|
|
738
|
+
index: Optional[int] = None,
|
|
739
|
+
parent: Optional[Locator] = None,
|
|
740
|
+
*,
|
|
741
|
+
content: str = 'true'
|
|
742
|
+
) -> bool:
|
|
743
|
+
"""元素可见性断言
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
targeting: 定位类型
|
|
747
|
+
element: 定位表达式
|
|
748
|
+
index: 索引
|
|
749
|
+
parent: 父元素定位器
|
|
750
|
+
content: 期望是否可见 (true/false)
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
bool: 断言结果
|
|
754
|
+
"""
|
|
755
|
+
expected = str(content).lower() in ('true', '1', 'yes')
|
|
756
|
+
loc = self.locator(targeting, element, index, parent)
|
|
757
|
+
is_visible = loc.is_visible()
|
|
758
|
+
result = is_visible == expected
|
|
759
|
+
INFO(f"可见性断言: {is_visible} == {expected} -> {result}")
|
|
760
|
+
return result
|
|
761
|
+
|
|
762
|
+
def element_enabled_assert(
|
|
763
|
+
self,
|
|
764
|
+
targeting: str,
|
|
765
|
+
element: str,
|
|
766
|
+
index: Optional[int] = None,
|
|
767
|
+
parent: Optional[Locator] = None,
|
|
768
|
+
*,
|
|
769
|
+
content: str = 'true'
|
|
770
|
+
) -> bool:
|
|
771
|
+
"""元素可用性断言
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
targeting: 定位类型
|
|
775
|
+
element: 定位表达式
|
|
776
|
+
index: 索引
|
|
777
|
+
parent: 父元素定位器
|
|
778
|
+
content: 期望是否可用 (true/false)
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
bool: 断言结果
|
|
782
|
+
"""
|
|
783
|
+
expected = str(content).lower() in ('true', '1', 'yes')
|
|
784
|
+
loc = self.locator(targeting, element, index, parent)
|
|
785
|
+
is_enabled = loc.is_enabled()
|
|
786
|
+
result = is_enabled == expected
|
|
787
|
+
INFO(f"可用性断言: {is_enabled} == {expected} -> {result}")
|
|
788
|
+
return result
|
|
789
|
+
|
|
790
|
+
def element_count_assert(
|
|
791
|
+
self,
|
|
792
|
+
targeting: str,
|
|
793
|
+
element: str,
|
|
794
|
+
*,
|
|
795
|
+
content: str,
|
|
796
|
+
mode: str = 'equals'
|
|
797
|
+
) -> bool:
|
|
798
|
+
"""元素数量断言
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
targeting: 定位类型
|
|
802
|
+
element: 定位表达式
|
|
803
|
+
content: 期望数量
|
|
804
|
+
mode: 断言模式 (equals, gt, lt, gte, lte)
|
|
805
|
+
|
|
806
|
+
Returns:
|
|
807
|
+
bool: 断言结果
|
|
808
|
+
"""
|
|
809
|
+
loc = self.locator(targeting, element)
|
|
810
|
+
actual = loc.count()
|
|
811
|
+
expected = int(content)
|
|
812
|
+
|
|
813
|
+
mode_map = {
|
|
814
|
+
'equals': actual == expected,
|
|
815
|
+
'gt': actual > expected,
|
|
816
|
+
'lt': actual < expected,
|
|
817
|
+
'gte': actual >= expected,
|
|
818
|
+
'lte': actual <= expected,
|
|
819
|
+
}
|
|
820
|
+
result = mode_map.get(mode, actual == expected)
|
|
821
|
+
INFO(f"数量断言: {actual} {mode} {expected} -> {result}")
|
|
822
|
+
return result
|
|
823
|
+
|
|
824
|
+
def _assert_text(self, actual: str, expected: str, mode: str) -> bool:
|
|
825
|
+
"""文本断言辅助方法
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
actual: 实际值
|
|
829
|
+
expected: 期望值
|
|
830
|
+
mode: 断言模式
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
bool: 断言结果
|
|
834
|
+
"""
|
|
835
|
+
if mode == 'equals':
|
|
836
|
+
return actual == expected
|
|
837
|
+
elif mode == 'contains':
|
|
838
|
+
return expected in actual
|
|
839
|
+
elif mode == 'regex':
|
|
840
|
+
return bool(re.search(expected, actual))
|
|
841
|
+
elif mode == 'startswith':
|
|
842
|
+
return actual.startswith(expected)
|
|
843
|
+
elif mode == 'endswith':
|
|
844
|
+
return actual.endswith(expected)
|
|
845
|
+
elif mode == 'not_equals':
|
|
846
|
+
return actual != expected
|
|
847
|
+
elif mode == 'not_contains':
|
|
848
|
+
return expected not in actual
|
|
849
|
+
return False
|
|
850
|
+
|
|
851
|
+
# ==================== 取值操作 ====================
|
|
852
|
+
|
|
853
|
+
def get_element_text(
|
|
854
|
+
self,
|
|
855
|
+
targeting: str,
|
|
856
|
+
element: str,
|
|
857
|
+
index: Optional[int] = None,
|
|
858
|
+
parent: Optional[Locator] = None,
|
|
859
|
+
*,
|
|
860
|
+
content: str
|
|
861
|
+
) -> str:
|
|
862
|
+
"""获取元素文本并缓存
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
targeting: 定位类型
|
|
866
|
+
element: 定位表达式
|
|
867
|
+
index: 索引
|
|
868
|
+
parent: 父元素定位器
|
|
869
|
+
content: 缓存键名
|
|
870
|
+
|
|
871
|
+
Returns:
|
|
872
|
+
str: 元素文本
|
|
873
|
+
"""
|
|
874
|
+
loc = self.locator(targeting, element, index, parent)
|
|
875
|
+
text = loc.text_content() or ''
|
|
876
|
+
text = text.strip()
|
|
877
|
+
set_element_value(content, text)
|
|
878
|
+
INFO(f"获取文本: {content} = '{text}'")
|
|
879
|
+
return text
|
|
880
|
+
|
|
881
|
+
def get_element_value(
|
|
882
|
+
self,
|
|
883
|
+
targeting: str,
|
|
884
|
+
element: str,
|
|
885
|
+
index: Optional[int] = None,
|
|
886
|
+
parent: Optional[Locator] = None,
|
|
887
|
+
*,
|
|
888
|
+
content: str
|
|
889
|
+
) -> str:
|
|
890
|
+
"""获取元素 value 属性并缓存
|
|
891
|
+
|
|
892
|
+
Args:
|
|
893
|
+
targeting: 定位类型
|
|
894
|
+
element: 定位表达式
|
|
895
|
+
index: 索引
|
|
896
|
+
parent: 父元素定位器
|
|
897
|
+
content: 缓存键名
|
|
898
|
+
|
|
899
|
+
Returns:
|
|
900
|
+
str: value 值
|
|
901
|
+
"""
|
|
902
|
+
loc = self.locator(targeting, element, index, parent)
|
|
903
|
+
value = loc.input_value() or ''
|
|
904
|
+
set_element_value(content, value)
|
|
905
|
+
INFO(f"获取 value: {content} = '{value}'")
|
|
906
|
+
return value
|
|
907
|
+
|
|
908
|
+
def get_element_attribute(
|
|
909
|
+
self,
|
|
910
|
+
targeting: str,
|
|
911
|
+
element: str,
|
|
912
|
+
index: Optional[int] = None,
|
|
913
|
+
parent: Optional[Locator] = None,
|
|
914
|
+
*,
|
|
915
|
+
attribute: str,
|
|
916
|
+
content: str
|
|
917
|
+
) -> str:
|
|
918
|
+
"""获取元素属性并缓存
|
|
919
|
+
|
|
920
|
+
Args:
|
|
921
|
+
targeting: 定位类型
|
|
922
|
+
element: 定位表达式
|
|
923
|
+
index: 索引
|
|
924
|
+
parent: 父元素定位器
|
|
925
|
+
attribute: 属性名
|
|
926
|
+
content: 缓存键名
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
str: 属性值
|
|
930
|
+
"""
|
|
931
|
+
loc = self.locator(targeting, element, index, parent)
|
|
932
|
+
value = loc.get_attribute(attribute) or ''
|
|
933
|
+
set_element_value(content, value)
|
|
934
|
+
INFO(f"获取属性: {content} = {attribute}='{value}'")
|
|
935
|
+
return value
|
|
936
|
+
|
|
937
|
+
def get_page_title(self, *, content: str) -> str:
|
|
938
|
+
"""获取页面标题并缓存
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
content: 缓存键名
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
str: 页面标题
|
|
945
|
+
"""
|
|
946
|
+
title = self.page.title()
|
|
947
|
+
set_element_value(content, title)
|
|
948
|
+
INFO(f"获取标题: {content} = '{title}'")
|
|
949
|
+
return title
|
|
950
|
+
|
|
951
|
+
def get_page_url(self, *, content: str) -> str:
|
|
952
|
+
"""获取页面 URL 并缓存
|
|
953
|
+
|
|
954
|
+
Args:
|
|
955
|
+
content: 缓存键名
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
str: 页面 URL
|
|
959
|
+
"""
|
|
960
|
+
url = self.page.url
|
|
961
|
+
set_element_value(content, url)
|
|
962
|
+
INFO(f"获取 URL: {content} = '{url}'")
|
|
963
|
+
return url
|
|
964
|
+
|
|
965
|
+
# ==================== 等待操作 ====================
|
|
966
|
+
|
|
967
|
+
def time_sleep(self, *, content: str) -> None:
|
|
968
|
+
"""显式等待
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
content: 等待秒数
|
|
972
|
+
"""
|
|
973
|
+
seconds = float(content)
|
|
974
|
+
INFO(f"等待 {seconds} 秒")
|
|
975
|
+
self.page.wait_for_timeout(int(seconds * 1000))
|
|
976
|
+
|
|
977
|
+
def wait_element_visible(
|
|
978
|
+
self,
|
|
979
|
+
targeting: str,
|
|
980
|
+
element: str,
|
|
981
|
+
index: Optional[int] = None,
|
|
982
|
+
*,
|
|
983
|
+
content: str = '30000'
|
|
984
|
+
) -> None:
|
|
985
|
+
"""等待元素可见
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
targeting: 定位类型
|
|
989
|
+
element: 定位表达式
|
|
990
|
+
index: 索引
|
|
991
|
+
content: 超时时间 (毫秒)
|
|
992
|
+
"""
|
|
993
|
+
timeout = int(content)
|
|
994
|
+
INFO(f"等待元素可见: {element}")
|
|
995
|
+
loc = self.locator(targeting, element, index)
|
|
996
|
+
loc.wait_for(state='visible', timeout=timeout)
|
|
997
|
+
|
|
998
|
+
def wait_element_hidden(
|
|
999
|
+
self,
|
|
1000
|
+
targeting: str,
|
|
1001
|
+
element: str,
|
|
1002
|
+
index: Optional[int] = None,
|
|
1003
|
+
*,
|
|
1004
|
+
content: str = '30000'
|
|
1005
|
+
) -> None:
|
|
1006
|
+
"""等待元素隐藏
|
|
1007
|
+
|
|
1008
|
+
Args:
|
|
1009
|
+
targeting: 定位类型
|
|
1010
|
+
element: 定位表达式
|
|
1011
|
+
index: 索引
|
|
1012
|
+
content: 超时时间 (毫秒)
|
|
1013
|
+
"""
|
|
1014
|
+
timeout = int(content)
|
|
1015
|
+
INFO(f"等待元素隐藏: {element}")
|
|
1016
|
+
loc = self.locator(targeting, element, index)
|
|
1017
|
+
loc.wait_for(state='hidden', timeout=timeout)
|
|
1018
|
+
|
|
1019
|
+
def wait_page_load(self, *, content: str = '30000') -> None:
|
|
1020
|
+
"""等待页面加载完成
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
content: 超时时间 (毫秒)
|
|
1024
|
+
"""
|
|
1025
|
+
timeout = int(content)
|
|
1026
|
+
INFO("等待页面加载")
|
|
1027
|
+
self.page.wait_for_load_state('networkidle', timeout=timeout)
|
|
1028
|
+
|
|
1029
|
+
# ==================== 截图 ====================
|
|
1030
|
+
|
|
1031
|
+
def screenshot(self, *, content: str, full_page: bool = False) -> None:
|
|
1032
|
+
"""截图
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
content: 保存路径
|
|
1036
|
+
full_page: 是否全页截图
|
|
1037
|
+
"""
|
|
1038
|
+
INFO(f"截图: {content}")
|
|
1039
|
+
self.page.screenshot(path=content, full_page=full_page)
|
|
1040
|
+
|
|
1041
|
+
def screenshot_full(self, *, content: str) -> None:
|
|
1042
|
+
"""全页截图
|
|
1043
|
+
|
|
1044
|
+
Args:
|
|
1045
|
+
content: 保存路径
|
|
1046
|
+
"""
|
|
1047
|
+
INFO(f"全页截图: {content}")
|
|
1048
|
+
self.page.screenshot(path=content, full_page=True)
|
|
1049
|
+
|
|
1050
|
+
def element_screenshot(
|
|
1051
|
+
self,
|
|
1052
|
+
targeting: str,
|
|
1053
|
+
element: str,
|
|
1054
|
+
index: Optional[int] = None,
|
|
1055
|
+
*,
|
|
1056
|
+
content: str
|
|
1057
|
+
) -> None:
|
|
1058
|
+
"""元素截图
|
|
1059
|
+
|
|
1060
|
+
Args:
|
|
1061
|
+
targeting: 定位类型
|
|
1062
|
+
element: 定位表达式
|
|
1063
|
+
index: 索引
|
|
1064
|
+
content: 保存路径
|
|
1065
|
+
"""
|
|
1066
|
+
INFO(f"元素截图: {content}")
|
|
1067
|
+
loc = self.locator(targeting, element, index)
|
|
1068
|
+
loc.screenshot(path=content)
|
|
1069
|
+
|
|
1070
|
+
# ==================== 其他操作 ====================
|
|
1071
|
+
|
|
1072
|
+
def execute_js(self, *, content: str) -> any:
|
|
1073
|
+
"""执行 JavaScript
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
content: JavaScript 代码
|
|
1077
|
+
|
|
1078
|
+
Returns:
|
|
1079
|
+
执行结果
|
|
1080
|
+
"""
|
|
1081
|
+
INFO(f"执行 JS: {content[:50]}...")
|
|
1082
|
+
return self.page.evaluate(content)
|
|
1083
|
+
|
|
1084
|
+
def execute_js_on_element(
|
|
1085
|
+
self,
|
|
1086
|
+
targeting: str,
|
|
1087
|
+
element: str,
|
|
1088
|
+
index: Optional[int] = None,
|
|
1089
|
+
*,
|
|
1090
|
+
content: str
|
|
1091
|
+
) -> any:
|
|
1092
|
+
"""在元素上执行 JavaScript
|
|
1093
|
+
|
|
1094
|
+
Args:
|
|
1095
|
+
targeting: 定位类型
|
|
1096
|
+
element: 定位表达式
|
|
1097
|
+
index: 索引
|
|
1098
|
+
content: JavaScript 代码
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
执行结果
|
|
1102
|
+
"""
|
|
1103
|
+
INFO(f"元素 JS: {content[:50]}...")
|
|
1104
|
+
loc = self.locator(targeting, element, index)
|
|
1105
|
+
return loc.evaluate(content)
|
|
1106
|
+
|
|
1107
|
+
def highlight_element(
|
|
1108
|
+
self,
|
|
1109
|
+
targeting: str,
|
|
1110
|
+
element: str,
|
|
1111
|
+
index: Optional[int] = None,
|
|
1112
|
+
*,
|
|
1113
|
+
content: str = '2'
|
|
1114
|
+
) -> None:
|
|
1115
|
+
"""高亮元素
|
|
1116
|
+
|
|
1117
|
+
Args:
|
|
1118
|
+
targeting: 定位类型
|
|
1119
|
+
element: 定位表达式
|
|
1120
|
+
index: 索引
|
|
1121
|
+
content: 高亮秒数
|
|
1122
|
+
"""
|
|
1123
|
+
loc = self.locator(targeting, element, index)
|
|
1124
|
+
loc.evaluate('''el => {
|
|
1125
|
+
const original = el.style.outline;
|
|
1126
|
+
el.style.outline = '3px solid red';
|
|
1127
|
+
setTimeout(() => el.style.outline = original, arguments[0] * 1000);
|
|
1128
|
+
}''', float(content))
|
|
1129
|
+
INFO(f"高亮元素: {element}")
|