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
kdtest_pw/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""KDTest-Playwright 关键字驱动测试框架
|
|
2
|
+
|
|
3
|
+
基于 Playwright 的关键字驱动测试框架,专为 Element Plus/Vue3 优化。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .product import __version__, __author__, __description__
|
|
7
|
+
from .reference import (
|
|
8
|
+
GSTORE,
|
|
9
|
+
GSDSTORE,
|
|
10
|
+
MODULEDATA,
|
|
11
|
+
PRIVATEDATA,
|
|
12
|
+
CASESDATA,
|
|
13
|
+
INFO,
|
|
14
|
+
get_global,
|
|
15
|
+
set_global,
|
|
16
|
+
get_element_value,
|
|
17
|
+
set_element_value,
|
|
18
|
+
clear_element_values,
|
|
19
|
+
)
|
|
20
|
+
from .common import (
|
|
21
|
+
DEFAULT_CONFIG,
|
|
22
|
+
BROWSER_TYPES,
|
|
23
|
+
LOCATOR_TYPES,
|
|
24
|
+
KEY_MAP,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# 版本信息
|
|
29
|
+
'__version__',
|
|
30
|
+
'__author__',
|
|
31
|
+
'__description__',
|
|
32
|
+
# 全局存储
|
|
33
|
+
'GSTORE',
|
|
34
|
+
'GSDSTORE',
|
|
35
|
+
'MODULEDATA',
|
|
36
|
+
'PRIVATEDATA',
|
|
37
|
+
'CASESDATA',
|
|
38
|
+
# 工具函数
|
|
39
|
+
'INFO',
|
|
40
|
+
'get_global',
|
|
41
|
+
'set_global',
|
|
42
|
+
'get_element_value',
|
|
43
|
+
'set_element_value',
|
|
44
|
+
'clear_element_values',
|
|
45
|
+
# 常量
|
|
46
|
+
'DEFAULT_CONFIG',
|
|
47
|
+
'BROWSER_TYPES',
|
|
48
|
+
'LOCATOR_TYPES',
|
|
49
|
+
'KEY_MAP',
|
|
50
|
+
]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""基础关键字类 - 元素定位和通用操作"""
|
|
2
|
+
|
|
3
|
+
from playwright.sync_api import Page, Locator, FrameLocator
|
|
4
|
+
from typing import Optional, Union, Literal, List
|
|
5
|
+
|
|
6
|
+
from ..common import LOCATOR_TYPES
|
|
7
|
+
from ..reference import INFO
|
|
8
|
+
|
|
9
|
+
LocatorType = Literal[
|
|
10
|
+
'id', 'name', 'class_name', 'class', 'css', 'xpath',
|
|
11
|
+
'text', 'role', 'data_testid', 'placeholder', 'link_text', 'partial_link_text'
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseKeyword:
|
|
16
|
+
"""基础关键字类
|
|
17
|
+
|
|
18
|
+
提供元素定位和通用操作方法,是所有关键字类的基类。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, page: Page):
|
|
22
|
+
"""初始化基础关键字
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
page: Playwright Page 实例
|
|
26
|
+
"""
|
|
27
|
+
self.page = page
|
|
28
|
+
self._current_frame: Optional[FrameLocator] = None
|
|
29
|
+
self._frame_stack: List[FrameLocator] = []
|
|
30
|
+
|
|
31
|
+
def locator(
|
|
32
|
+
self,
|
|
33
|
+
targeting: LocatorType,
|
|
34
|
+
element: str,
|
|
35
|
+
index: Optional[Union[int, str]] = None,
|
|
36
|
+
parent: Optional[Locator] = None
|
|
37
|
+
) -> Locator:
|
|
38
|
+
"""统一元素定位方法
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
targeting: 定位类型 (id, name, class_name, css, xpath, text, role, data_testid, placeholder)
|
|
42
|
+
element: 定位表达式
|
|
43
|
+
index: 索引 (数字、'first'、'last'),None 表示不使用索引
|
|
44
|
+
parent: 父元素定位器,用于链式定位
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Locator: Playwright Locator 实例
|
|
48
|
+
"""
|
|
49
|
+
# 确定基础定位上下文
|
|
50
|
+
base = parent or self._current_frame or self.page
|
|
51
|
+
|
|
52
|
+
# 标准化定位类型
|
|
53
|
+
targeting = LOCATOR_TYPES.get(targeting, targeting)
|
|
54
|
+
|
|
55
|
+
# 根据定位类型创建 Locator
|
|
56
|
+
locator_map = {
|
|
57
|
+
'id': lambda: base.locator(f'#{element}'),
|
|
58
|
+
'name': lambda: base.locator(f'[name="{element}"]'),
|
|
59
|
+
'class_name': lambda: base.locator(f'.{element}'),
|
|
60
|
+
'css': lambda: base.locator(element),
|
|
61
|
+
'xpath': lambda: base.locator(f'xpath={element}'),
|
|
62
|
+
'text': lambda: base.get_by_text(element, exact=False),
|
|
63
|
+
'role': lambda: base.get_by_role(element),
|
|
64
|
+
'data_testid': lambda: base.get_by_test_id(element),
|
|
65
|
+
'placeholder': lambda: base.get_by_placeholder(element),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
loc = locator_map.get(targeting, lambda: base.locator(element))()
|
|
69
|
+
|
|
70
|
+
# 处理索引
|
|
71
|
+
if index is not None:
|
|
72
|
+
if index == 'first' or index == 0:
|
|
73
|
+
loc = loc.first
|
|
74
|
+
elif index == 'last' or index == -1:
|
|
75
|
+
loc = loc.last
|
|
76
|
+
elif isinstance(index, int):
|
|
77
|
+
loc = loc.nth(index)
|
|
78
|
+
elif isinstance(index, str) and index.isdigit():
|
|
79
|
+
loc = loc.nth(int(index))
|
|
80
|
+
|
|
81
|
+
return loc
|
|
82
|
+
|
|
83
|
+
def locator_by_expression(self, expression: str, index: Optional[Union[int, str]] = None) -> Locator:
|
|
84
|
+
"""通过表达式定位元素
|
|
85
|
+
|
|
86
|
+
支持格式:
|
|
87
|
+
- "xpath=//div" 或 "xpath://div"
|
|
88
|
+
- "css=.class" 或 "css:.class"
|
|
89
|
+
- "text=文本"
|
|
90
|
+
- "id=elementId"
|
|
91
|
+
- 纯 CSS 选择器
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
expression: 定位表达式
|
|
95
|
+
index: 索引
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Locator: Playwright Locator 实例
|
|
99
|
+
"""
|
|
100
|
+
base = self._current_frame or self.page
|
|
101
|
+
|
|
102
|
+
# 解析表达式
|
|
103
|
+
if '=' in expression:
|
|
104
|
+
parts = expression.split('=', 1)
|
|
105
|
+
prefix = parts[0].lower().rstrip(':')
|
|
106
|
+
value = parts[1]
|
|
107
|
+
|
|
108
|
+
if prefix == 'xpath':
|
|
109
|
+
loc = base.locator(f'xpath={value}')
|
|
110
|
+
elif prefix == 'css':
|
|
111
|
+
loc = base.locator(value)
|
|
112
|
+
elif prefix == 'text':
|
|
113
|
+
loc = base.get_by_text(value)
|
|
114
|
+
elif prefix == 'id':
|
|
115
|
+
loc = base.locator(f'#{value}')
|
|
116
|
+
elif prefix == 'name':
|
|
117
|
+
loc = base.locator(f'[name="{value}"]')
|
|
118
|
+
elif prefix == 'class':
|
|
119
|
+
loc = base.locator(f'.{value}')
|
|
120
|
+
else:
|
|
121
|
+
loc = base.locator(expression)
|
|
122
|
+
else:
|
|
123
|
+
# 纯 CSS 选择器
|
|
124
|
+
loc = base.locator(expression)
|
|
125
|
+
|
|
126
|
+
# 处理索引
|
|
127
|
+
if index is not None:
|
|
128
|
+
if index == 'first' or index == 0:
|
|
129
|
+
loc = loc.first
|
|
130
|
+
elif index == 'last' or index == -1:
|
|
131
|
+
loc = loc.last
|
|
132
|
+
elif isinstance(index, int):
|
|
133
|
+
loc = loc.nth(index)
|
|
134
|
+
|
|
135
|
+
return loc
|
|
136
|
+
|
|
137
|
+
def switch_frame(self, targeting: str, element: str) -> None:
|
|
138
|
+
"""切换到 iframe
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
targeting: 定位类型
|
|
142
|
+
element: 定位表达式
|
|
143
|
+
"""
|
|
144
|
+
if targeting == 'xpath':
|
|
145
|
+
frame_locator = self.page.frame_locator(f'xpath={element}')
|
|
146
|
+
elif targeting in ('css', 'selector'):
|
|
147
|
+
frame_locator = self.page.frame_locator(element)
|
|
148
|
+
elif targeting == 'name':
|
|
149
|
+
frame_locator = self.page.frame_locator(f'[name="{element}"]')
|
|
150
|
+
elif targeting == 'id':
|
|
151
|
+
frame_locator = self.page.frame_locator(f'#{element}')
|
|
152
|
+
else:
|
|
153
|
+
frame_locator = self.page.frame_locator(element)
|
|
154
|
+
|
|
155
|
+
self._frame_stack.append(self._current_frame)
|
|
156
|
+
self._current_frame = frame_locator
|
|
157
|
+
INFO(f"切换到 iframe: {element}")
|
|
158
|
+
|
|
159
|
+
def switch_frame_parent(self) -> None:
|
|
160
|
+
"""切换到父级 frame"""
|
|
161
|
+
if self._frame_stack:
|
|
162
|
+
self._current_frame = self._frame_stack.pop()
|
|
163
|
+
else:
|
|
164
|
+
self._current_frame = None
|
|
165
|
+
INFO("切换到父级 frame")
|
|
166
|
+
|
|
167
|
+
def frame_default(self) -> None:
|
|
168
|
+
"""返回主文档"""
|
|
169
|
+
self._current_frame = None
|
|
170
|
+
self._frame_stack.clear()
|
|
171
|
+
INFO("切换到主文档")
|
|
172
|
+
|
|
173
|
+
def wait_for_loading(self, timeout: int = 30000) -> None:
|
|
174
|
+
"""等待页面加载完成
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
timeout: 超时时间 (毫秒)
|
|
178
|
+
"""
|
|
179
|
+
self.page.wait_for_load_state('networkidle', timeout=timeout)
|
|
180
|
+
|
|
181
|
+
def wait_for_dom_ready(self, timeout: int = 30000) -> None:
|
|
182
|
+
"""等待 DOM 加载完成
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
timeout: 超时时间 (毫秒)
|
|
186
|
+
"""
|
|
187
|
+
self.page.wait_for_load_state('domcontentloaded', timeout=timeout)
|
|
188
|
+
|
|
189
|
+
def wait_for_element_plus_loading(self, timeout: int = 30000) -> None:
|
|
190
|
+
"""等待 Element Plus 的 v-loading 消失
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
timeout: 超时时间 (毫秒)
|
|
194
|
+
"""
|
|
195
|
+
loading = self.page.locator('.el-loading-mask')
|
|
196
|
+
try:
|
|
197
|
+
if loading.count() > 0:
|
|
198
|
+
loading.first.wait_for(state='hidden', timeout=timeout)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass # loading 可能已经消失
|
|
201
|
+
|
|
202
|
+
def wait_for_element(
|
|
203
|
+
self,
|
|
204
|
+
targeting: str,
|
|
205
|
+
element: str,
|
|
206
|
+
state: str = 'visible',
|
|
207
|
+
timeout: int = 30000
|
|
208
|
+
) -> None:
|
|
209
|
+
"""等待元素状态
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
targeting: 定位类型
|
|
213
|
+
element: 定位表达式
|
|
214
|
+
state: 等待状态 (visible, hidden, attached, detached)
|
|
215
|
+
timeout: 超时时间 (毫秒)
|
|
216
|
+
"""
|
|
217
|
+
loc = self.locator(targeting, element)
|
|
218
|
+
loc.wait_for(state=state, timeout=timeout)
|
|
219
|
+
|
|
220
|
+
def is_element_visible(self, targeting: str, element: str, index: Optional[int] = None) -> bool:
|
|
221
|
+
"""检查元素是否可见
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
targeting: 定位类型
|
|
225
|
+
element: 定位表达式
|
|
226
|
+
index: 索引
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
bool: 元素是否可见
|
|
230
|
+
"""
|
|
231
|
+
loc = self.locator(targeting, element, index)
|
|
232
|
+
return loc.is_visible()
|
|
233
|
+
|
|
234
|
+
def is_element_enabled(self, targeting: str, element: str, index: Optional[int] = None) -> bool:
|
|
235
|
+
"""检查元素是否可用
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
targeting: 定位类型
|
|
239
|
+
element: 定位表达式
|
|
240
|
+
index: 索引
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
bool: 元素是否可用
|
|
244
|
+
"""
|
|
245
|
+
loc = self.locator(targeting, element, index)
|
|
246
|
+
return loc.is_enabled()
|
|
247
|
+
|
|
248
|
+
def get_element_count(self, targeting: str, element: str) -> int:
|
|
249
|
+
"""获取元素数量
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
targeting: 定位类型
|
|
253
|
+
element: 定位表达式
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
int: 元素数量
|
|
257
|
+
"""
|
|
258
|
+
loc = self.locator(targeting, element)
|
|
259
|
+
return loc.count()
|
|
260
|
+
|
|
261
|
+
def execute_script(self, script: str, *args) -> any:
|
|
262
|
+
"""执行 JavaScript
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
script: JavaScript 代码
|
|
266
|
+
*args: 脚本参数
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
脚本执行结果
|
|
270
|
+
"""
|
|
271
|
+
return self.page.evaluate(script, args if args else None)
|
|
272
|
+
|
|
273
|
+
def execute_script_on_element(
|
|
274
|
+
self,
|
|
275
|
+
targeting: str,
|
|
276
|
+
element: str,
|
|
277
|
+
script: str,
|
|
278
|
+
index: Optional[int] = None
|
|
279
|
+
) -> any:
|
|
280
|
+
"""在元素上执行 JavaScript
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
targeting: 定位类型
|
|
284
|
+
element: 定位表达式
|
|
285
|
+
script: JavaScript 代码 (使用 el 引用元素)
|
|
286
|
+
index: 索引
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
脚本执行结果
|
|
290
|
+
"""
|
|
291
|
+
loc = self.locator(targeting, element, index)
|
|
292
|
+
return loc.evaluate(script)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Element Plus 专用关键字模块"""
|
|
2
|
+
|
|
3
|
+
from .el_select import ElSelectKeyword
|
|
4
|
+
from .el_datepicker import ElDatePickerKeyword
|
|
5
|
+
from .el_dialog import ElDialogKeyword
|
|
6
|
+
from .el_table import ElTableKeyword
|
|
7
|
+
from .el_cascader import ElCascaderKeyword
|
|
8
|
+
from .el_tree import ElTreeKeyword
|
|
9
|
+
from .el_upload import ElUploadKeyword
|
|
10
|
+
from .el_form import ElFormKeyword
|
|
11
|
+
from .el_menu import ElMenuKeyword
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'ElSelectKeyword',
|
|
15
|
+
'ElDatePickerKeyword',
|
|
16
|
+
'ElDialogKeyword',
|
|
17
|
+
'ElTableKeyword',
|
|
18
|
+
'ElCascaderKeyword',
|
|
19
|
+
'ElTreeKeyword',
|
|
20
|
+
'ElUploadKeyword',
|
|
21
|
+
'ElFormKeyword',
|
|
22
|
+
'ElMenuKeyword',
|
|
23
|
+
]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Element Plus Cascader 组件关键字"""
|
|
2
|
+
|
|
3
|
+
from playwright.sync_api import Page, Locator
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
|
|
6
|
+
from ..base_keyword import BaseKeyword
|
|
7
|
+
from ...reference import INFO, set_element_value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ElCascaderKeyword(BaseKeyword):
|
|
11
|
+
"""Element Plus Cascader 级联选择器关键字
|
|
12
|
+
|
|
13
|
+
处理级联选择器的多级选择、搜索等操作。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, page: Page):
|
|
17
|
+
super().__init__(page)
|
|
18
|
+
|
|
19
|
+
def el_cascader(
|
|
20
|
+
self,
|
|
21
|
+
targeting: str,
|
|
22
|
+
element: str,
|
|
23
|
+
index: Optional[int] = None,
|
|
24
|
+
*,
|
|
25
|
+
content: str
|
|
26
|
+
) -> None:
|
|
27
|
+
"""级联选择
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
targeting: 定位类型
|
|
31
|
+
element: 定位表达式
|
|
32
|
+
index: 索引
|
|
33
|
+
content: 选项路径 (格式: "一级/二级/三级")
|
|
34
|
+
"""
|
|
35
|
+
options = [opt.strip() for opt in str(content).split('/')]
|
|
36
|
+
INFO(f"级联选择: {options}")
|
|
37
|
+
|
|
38
|
+
cascader = self.locator(targeting, element, index)
|
|
39
|
+
cascader.click()
|
|
40
|
+
|
|
41
|
+
# 等待面板出现
|
|
42
|
+
panel = self.page.locator('.el-cascader-panel:visible, .el-cascader__dropdown:visible')
|
|
43
|
+
panel.wait_for(state='visible', timeout=5000)
|
|
44
|
+
|
|
45
|
+
# 逐级选择
|
|
46
|
+
for i, opt in enumerate(options):
|
|
47
|
+
# 找到当前级别的菜单
|
|
48
|
+
menus = panel.locator('.el-cascader-menu')
|
|
49
|
+
if menus.count() > i:
|
|
50
|
+
menu = menus.nth(i)
|
|
51
|
+
node = menu.locator(f'.el-cascader-node:has-text("{opt}")')
|
|
52
|
+
if node.count() > 0:
|
|
53
|
+
node.first.click()
|
|
54
|
+
self.page.wait_for_timeout(200)
|
|
55
|
+
|
|
56
|
+
# 关闭面板
|
|
57
|
+
self.page.keyboard.press('Escape')
|
|
58
|
+
|
|
59
|
+
def el_cascader_search(
|
|
60
|
+
self,
|
|
61
|
+
targeting: str,
|
|
62
|
+
element: str,
|
|
63
|
+
index: Optional[int] = None,
|
|
64
|
+
*,
|
|
65
|
+
content: str,
|
|
66
|
+
select_first: bool = True
|
|
67
|
+
) -> None:
|
|
68
|
+
"""搜索并选择
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
targeting: 定位类型
|
|
72
|
+
element: 定位表达式
|
|
73
|
+
index: 索引
|
|
74
|
+
content: 搜索关键词
|
|
75
|
+
select_first: 是否选择第一个结果
|
|
76
|
+
"""
|
|
77
|
+
INFO(f"级联搜索: {content}")
|
|
78
|
+
|
|
79
|
+
cascader = self.locator(targeting, element, index)
|
|
80
|
+
cascader.click()
|
|
81
|
+
|
|
82
|
+
# 输入搜索
|
|
83
|
+
input_el = cascader.locator('.el-input__inner')
|
|
84
|
+
input_el.fill(str(content))
|
|
85
|
+
self.page.wait_for_timeout(300)
|
|
86
|
+
|
|
87
|
+
# 选择结果
|
|
88
|
+
if select_first:
|
|
89
|
+
suggestion = self.page.locator('.el-cascader__suggestion-panel:visible, .el-cascader-panel:visible')
|
|
90
|
+
if suggestion.is_visible():
|
|
91
|
+
first_item = suggestion.locator('.el-cascader__suggestion-item, .el-cascader-node').first
|
|
92
|
+
if first_item.is_visible():
|
|
93
|
+
first_item.click()
|
|
94
|
+
|
|
95
|
+
def el_cascader_clear(
|
|
96
|
+
self,
|
|
97
|
+
targeting: str,
|
|
98
|
+
element: str,
|
|
99
|
+
index: Optional[int] = None
|
|
100
|
+
) -> None:
|
|
101
|
+
"""清除选择
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
targeting: 定位类型
|
|
105
|
+
element: 定位表达式
|
|
106
|
+
index: 索引
|
|
107
|
+
"""
|
|
108
|
+
INFO("清除级联选择")
|
|
109
|
+
|
|
110
|
+
cascader = self.locator(targeting, element, index)
|
|
111
|
+
cascader.hover()
|
|
112
|
+
self.page.wait_for_timeout(200)
|
|
113
|
+
|
|
114
|
+
clear_btn = cascader.locator('.el-cascader__clear-icon, .el-icon-circle-close')
|
|
115
|
+
if clear_btn.is_visible():
|
|
116
|
+
clear_btn.click()
|
|
117
|
+
|
|
118
|
+
def el_cascader_get_value(
|
|
119
|
+
self,
|
|
120
|
+
targeting: str,
|
|
121
|
+
element: str,
|
|
122
|
+
index: Optional[int] = None
|
|
123
|
+
) -> str:
|
|
124
|
+
"""获取当前选中值
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
targeting: 定位类型
|
|
128
|
+
element: 定位表达式
|
|
129
|
+
index: 索引
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
str: 选中的值(路径格式)
|
|
133
|
+
"""
|
|
134
|
+
cascader = self.locator(targeting, element, index)
|
|
135
|
+
|
|
136
|
+
# 尝试获取标签
|
|
137
|
+
tags = cascader.locator('.el-cascader__tags .el-tag')
|
|
138
|
+
if tags.count() > 0:
|
|
139
|
+
values = [tag.text_content().strip() for tag in tags.all()]
|
|
140
|
+
return ' / '.join(values)
|
|
141
|
+
|
|
142
|
+
# 获取输入框值
|
|
143
|
+
input_el = cascader.locator('.el-input__inner')
|
|
144
|
+
value = input_el.get_attribute('value') or input_el.text_content()
|
|
145
|
+
return (value or '').strip()
|
|
146
|
+
|
|
147
|
+
def el_cascader_expand(
|
|
148
|
+
self,
|
|
149
|
+
targeting: str,
|
|
150
|
+
element: str,
|
|
151
|
+
index: Optional[int] = None,
|
|
152
|
+
*,
|
|
153
|
+
content: str
|
|
154
|
+
) -> None:
|
|
155
|
+
"""展开到指定节点(不选择)
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
targeting: 定位类型
|
|
159
|
+
element: 定位表达式
|
|
160
|
+
index: 索引
|
|
161
|
+
content: 展开路径 (格式: "一级/二级")
|
|
162
|
+
"""
|
|
163
|
+
options = [opt.strip() for opt in str(content).split('/')]
|
|
164
|
+
INFO(f"展开级联: {options}")
|
|
165
|
+
|
|
166
|
+
cascader = self.locator(targeting, element, index)
|
|
167
|
+
cascader.click()
|
|
168
|
+
|
|
169
|
+
panel = self.page.locator('.el-cascader-panel:visible')
|
|
170
|
+
panel.wait_for(state='visible', timeout=5000)
|
|
171
|
+
|
|
172
|
+
# 逐级展开(不选择叶子节点)
|
|
173
|
+
for i, opt in enumerate(options):
|
|
174
|
+
menus = panel.locator('.el-cascader-menu')
|
|
175
|
+
if menus.count() > i:
|
|
176
|
+
menu = menus.nth(i)
|
|
177
|
+
# 悬停展开
|
|
178
|
+
node = menu.locator(f'.el-cascader-node:has-text("{opt}")')
|
|
179
|
+
if node.count() > 0:
|
|
180
|
+
node.first.hover()
|
|
181
|
+
self.page.wait_for_timeout(200)
|
|
182
|
+
|
|
183
|
+
def el_cascader_select_multiple(
|
|
184
|
+
self,
|
|
185
|
+
targeting: str,
|
|
186
|
+
element: str,
|
|
187
|
+
index: Optional[int] = None,
|
|
188
|
+
*,
|
|
189
|
+
content: str
|
|
190
|
+
) -> None:
|
|
191
|
+
"""多选级联
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
targeting: 定位类型
|
|
195
|
+
element: 定位表达式
|
|
196
|
+
index: 索引
|
|
197
|
+
content: 多个路径 (格式: "一级/二级,一级/三级")
|
|
198
|
+
"""
|
|
199
|
+
paths = [path.strip() for path in str(content).split(',')]
|
|
200
|
+
INFO(f"级联多选: {paths}")
|
|
201
|
+
|
|
202
|
+
cascader = self.locator(targeting, element, index)
|
|
203
|
+
cascader.click()
|
|
204
|
+
|
|
205
|
+
panel = self.page.locator('.el-cascader-panel:visible')
|
|
206
|
+
panel.wait_for(state='visible', timeout=5000)
|
|
207
|
+
|
|
208
|
+
for path in paths:
|
|
209
|
+
options = [opt.strip() for opt in path.split('/')]
|
|
210
|
+
|
|
211
|
+
# 逐级选择
|
|
212
|
+
for i, opt in enumerate(options):
|
|
213
|
+
menus = panel.locator('.el-cascader-menu')
|
|
214
|
+
if menus.count() > i:
|
|
215
|
+
menu = menus.nth(i)
|
|
216
|
+
node = menu.locator(f'.el-cascader-node:has-text("{opt}")')
|
|
217
|
+
if node.count() > 0:
|
|
218
|
+
# 如果是最后一级,点击复选框
|
|
219
|
+
if i == len(options) - 1:
|
|
220
|
+
checkbox = node.first.locator('.el-checkbox')
|
|
221
|
+
if checkbox.count() > 0:
|
|
222
|
+
checkbox.click()
|
|
223
|
+
else:
|
|
224
|
+
node.first.click()
|
|
225
|
+
else:
|
|
226
|
+
node.first.click()
|
|
227
|
+
self.page.wait_for_timeout(200)
|
|
228
|
+
|
|
229
|
+
# 关闭面板
|
|
230
|
+
self.page.keyboard.press('Escape')
|
|
231
|
+
|
|
232
|
+
def el_cascader_assert(
|
|
233
|
+
self,
|
|
234
|
+
targeting: str,
|
|
235
|
+
element: str,
|
|
236
|
+
index: Optional[int] = None,
|
|
237
|
+
*,
|
|
238
|
+
content: str,
|
|
239
|
+
mode: str = 'equals'
|
|
240
|
+
) -> bool:
|
|
241
|
+
"""断言选中值
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
targeting: 定位类型
|
|
245
|
+
element: 定位表达式
|
|
246
|
+
index: 索引
|
|
247
|
+
content: 期望值
|
|
248
|
+
mode: 断言模式
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
bool: 断言结果
|
|
252
|
+
"""
|
|
253
|
+
actual = self.el_cascader_get_value(targeting, element, index)
|
|
254
|
+
|
|
255
|
+
if mode == 'equals':
|
|
256
|
+
result = actual == content
|
|
257
|
+
elif mode == 'contains':
|
|
258
|
+
result = content in actual
|
|
259
|
+
else:
|
|
260
|
+
result = actual == content
|
|
261
|
+
|
|
262
|
+
INFO(f"级联断言: '{actual}' {mode} '{content}' -> {result}")
|
|
263
|
+
return result
|