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,311 @@
|
|
|
1
|
+
"""关键字分发器"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional, Callable, List, Tuple
|
|
4
|
+
from playwright.sync_api import Page
|
|
5
|
+
|
|
6
|
+
from .page_action import KeyWordTest
|
|
7
|
+
from ..reference import GSTORE, MODULEDATA, INFO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KeyRetrieval:
|
|
11
|
+
"""关键字分发器
|
|
12
|
+
|
|
13
|
+
负责解析 Excel 中的关键字并分发到对应的方法执行。
|
|
14
|
+
支持插件扩展关键字。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, page: Optional[Page] = None):
|
|
18
|
+
"""初始化关键字分发器
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
page: Playwright Page 实例
|
|
22
|
+
"""
|
|
23
|
+
self._page = page or GSTORE.get('page')
|
|
24
|
+
self._keyword_test: Optional[KeyWordTest] = None
|
|
25
|
+
self._plugin_keywords: Dict[str, Callable] = {}
|
|
26
|
+
self._element_data: Dict[str, Any] = {}
|
|
27
|
+
|
|
28
|
+
if self._page:
|
|
29
|
+
self._keyword_test = KeyWordTest(self._page)
|
|
30
|
+
GSTORE['keyWord'] = self._keyword_test
|
|
31
|
+
|
|
32
|
+
def set_page(self, page: Page) -> None:
|
|
33
|
+
"""设置页面实例
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
page: Playwright Page 实例
|
|
37
|
+
"""
|
|
38
|
+
self._page = page
|
|
39
|
+
self._keyword_test = KeyWordTest(page)
|
|
40
|
+
GSTORE['keyWord'] = self._keyword_test
|
|
41
|
+
|
|
42
|
+
def register_plugin_keyword(self, name: str, handler: Callable) -> None:
|
|
43
|
+
"""注册插件关键字
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: 关键字名称
|
|
47
|
+
handler: 处理函数
|
|
48
|
+
"""
|
|
49
|
+
self._plugin_keywords[name] = handler
|
|
50
|
+
INFO(f"注册插件关键字: {name}")
|
|
51
|
+
|
|
52
|
+
def register_plugin_keywords(self, keywords: Dict[str, Callable]) -> None:
|
|
53
|
+
"""批量注册插件关键字
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
keywords: 关键字字典 {name: handler}
|
|
57
|
+
"""
|
|
58
|
+
self._plugin_keywords.update(keywords)
|
|
59
|
+
INFO(f"注册插件关键字: {list(keywords.keys())}")
|
|
60
|
+
|
|
61
|
+
def set_element_data(self, data: Dict[str, Any]) -> None:
|
|
62
|
+
"""设置元素数据(来自 elementData.yaml)
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
data: 元素数据字典
|
|
66
|
+
"""
|
|
67
|
+
self._element_data = data
|
|
68
|
+
|
|
69
|
+
def resolve_element(self, element_path: str) -> Tuple[str, str, Optional[int]]:
|
|
70
|
+
"""解析元素路径
|
|
71
|
+
|
|
72
|
+
支持格式:
|
|
73
|
+
- "module/element" - 从 elementData 获取
|
|
74
|
+
- "module/element[0]" - 带索引
|
|
75
|
+
- "xpath://div" - 直接定位表达式
|
|
76
|
+
- "css:.class" - 直接定位表达式
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
element_path: 元素路径
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Tuple[targeting, element, index]: 定位类型、定位表达式、索引
|
|
83
|
+
"""
|
|
84
|
+
index = None
|
|
85
|
+
|
|
86
|
+
# 检查是否有索引
|
|
87
|
+
if '[' in element_path and element_path.endswith(']'):
|
|
88
|
+
bracket_pos = element_path.rfind('[')
|
|
89
|
+
index_str = element_path[bracket_pos + 1:-1]
|
|
90
|
+
element_path = element_path[:bracket_pos]
|
|
91
|
+
try:
|
|
92
|
+
index = int(index_str)
|
|
93
|
+
except ValueError:
|
|
94
|
+
if index_str == 'first':
|
|
95
|
+
index = 0
|
|
96
|
+
elif index_str == 'last':
|
|
97
|
+
index = -1
|
|
98
|
+
|
|
99
|
+
# 检查是否是直接定位表达式
|
|
100
|
+
if '=' in element_path or element_path.startswith('//'):
|
|
101
|
+
if element_path.startswith('xpath=') or element_path.startswith('xpath:') or element_path.startswith('//'):
|
|
102
|
+
if element_path.startswith('xpath=') or element_path.startswith('xpath:'):
|
|
103
|
+
return 'xpath', element_path.split('=', 1)[1] if '=' in element_path else element_path[6:], index
|
|
104
|
+
return 'xpath', element_path, index
|
|
105
|
+
elif element_path.startswith('css=') or element_path.startswith('css:'):
|
|
106
|
+
return 'css', element_path.split('=', 1)[1] if '=' in element_path else element_path[4:], index
|
|
107
|
+
elif element_path.startswith('id='):
|
|
108
|
+
return 'id', element_path[3:], index
|
|
109
|
+
elif element_path.startswith('name='):
|
|
110
|
+
return 'name', element_path[5:], index
|
|
111
|
+
elif element_path.startswith('text='):
|
|
112
|
+
return 'text', element_path[5:], index
|
|
113
|
+
|
|
114
|
+
# 从 elementData 解析
|
|
115
|
+
if '/' in element_path:
|
|
116
|
+
parts = element_path.split('/')
|
|
117
|
+
if len(parts) >= 2:
|
|
118
|
+
module = parts[0]
|
|
119
|
+
element_name = '/'.join(parts[1:])
|
|
120
|
+
|
|
121
|
+
# 在 elementData 中查找
|
|
122
|
+
if module in self._element_data:
|
|
123
|
+
module_data = self._element_data[module]
|
|
124
|
+
if element_name in module_data:
|
|
125
|
+
element_info = module_data[element_name]
|
|
126
|
+
if isinstance(element_info, list) and len(element_info) >= 2:
|
|
127
|
+
return element_info[0], element_info[1], index
|
|
128
|
+
|
|
129
|
+
# 作为 CSS 选择器处理
|
|
130
|
+
return 'css', element_path, index
|
|
131
|
+
|
|
132
|
+
def execute(
|
|
133
|
+
self,
|
|
134
|
+
keyword: str,
|
|
135
|
+
locator_info: List[str] = None,
|
|
136
|
+
value: str = None,
|
|
137
|
+
**kwargs
|
|
138
|
+
) -> Any:
|
|
139
|
+
"""执行关键字
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
keyword: 关键字名称
|
|
143
|
+
locator_info: 定位信息列表 [element_path, ...] 或 [targeting, element, ...]
|
|
144
|
+
value: 操作值
|
|
145
|
+
**kwargs: 其他参数
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
执行结果
|
|
149
|
+
"""
|
|
150
|
+
if not self._keyword_test:
|
|
151
|
+
raise RuntimeError("KeyWordTest 未初始化,请先调用 set_page()")
|
|
152
|
+
|
|
153
|
+
# 解析定位信息
|
|
154
|
+
targeting = None
|
|
155
|
+
element = None
|
|
156
|
+
index = None
|
|
157
|
+
|
|
158
|
+
if locator_info:
|
|
159
|
+
# 过滤空值
|
|
160
|
+
locator_info = [str(item).strip() for item in locator_info if item and str(item).strip()]
|
|
161
|
+
|
|
162
|
+
if len(locator_info) >= 1:
|
|
163
|
+
# 检查是否是 elementData 路径
|
|
164
|
+
first = locator_info[0]
|
|
165
|
+
if '/' in first or first.startswith('//') or '=' in first:
|
|
166
|
+
targeting, element, index = self.resolve_element(first)
|
|
167
|
+
# 如果有第二个参数且是数字,作为索引
|
|
168
|
+
if len(locator_info) >= 2 and locator_info[1]:
|
|
169
|
+
try:
|
|
170
|
+
index = int(locator_info[1])
|
|
171
|
+
except ValueError:
|
|
172
|
+
pass
|
|
173
|
+
else:
|
|
174
|
+
# 传统格式: [targeting, element, index?]
|
|
175
|
+
targeting = first
|
|
176
|
+
if len(locator_info) >= 2:
|
|
177
|
+
element = locator_info[1]
|
|
178
|
+
if len(locator_info) >= 3 and locator_info[2]:
|
|
179
|
+
try:
|
|
180
|
+
index = int(locator_info[2])
|
|
181
|
+
except ValueError:
|
|
182
|
+
if locator_info[2] == 'first':
|
|
183
|
+
index = 0
|
|
184
|
+
elif locator_info[2] == 'last':
|
|
185
|
+
index = -1
|
|
186
|
+
|
|
187
|
+
# 构建参数
|
|
188
|
+
params = {}
|
|
189
|
+
if targeting:
|
|
190
|
+
params['targeting'] = targeting
|
|
191
|
+
if element:
|
|
192
|
+
params['element'] = element
|
|
193
|
+
if index is not None:
|
|
194
|
+
params['index'] = index
|
|
195
|
+
if value is not None and value != '':
|
|
196
|
+
params['content'] = str(value)
|
|
197
|
+
|
|
198
|
+
# 合并额外参数
|
|
199
|
+
params.update(kwargs)
|
|
200
|
+
|
|
201
|
+
# 查找并执行关键字
|
|
202
|
+
# 1. 先检查插件关键字
|
|
203
|
+
if keyword in self._plugin_keywords:
|
|
204
|
+
handler = self._plugin_keywords[keyword]
|
|
205
|
+
return self._call_handler(handler, params)
|
|
206
|
+
|
|
207
|
+
# 2. 检查核心关键字
|
|
208
|
+
if hasattr(self._keyword_test, keyword):
|
|
209
|
+
method = getattr(self._keyword_test, keyword)
|
|
210
|
+
return self._call_method(method, params)
|
|
211
|
+
|
|
212
|
+
# 3. 关键字不存在
|
|
213
|
+
raise ValueError(f"未知关键字: {keyword}")
|
|
214
|
+
|
|
215
|
+
def _call_method(self, method: Callable, params: Dict[str, Any]) -> Any:
|
|
216
|
+
"""调用方法
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
method: 方法
|
|
220
|
+
params: 参数字典
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
执行结果
|
|
224
|
+
"""
|
|
225
|
+
import inspect
|
|
226
|
+
sig = inspect.signature(method)
|
|
227
|
+
|
|
228
|
+
# 分离位置参数和关键字参数
|
|
229
|
+
positional_args = []
|
|
230
|
+
keyword_args = {}
|
|
231
|
+
|
|
232
|
+
for name, param in sig.parameters.items():
|
|
233
|
+
if name in ('self',):
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if param.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
237
|
+
# 关键字参数
|
|
238
|
+
if name in params:
|
|
239
|
+
keyword_args[name] = params[name]
|
|
240
|
+
elif param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY):
|
|
241
|
+
# 位置参数
|
|
242
|
+
if name in params:
|
|
243
|
+
positional_args.append(params[name])
|
|
244
|
+
elif param.default is not inspect.Parameter.empty:
|
|
245
|
+
# 有默认值,跳过
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
return method(*positional_args, **keyword_args)
|
|
249
|
+
|
|
250
|
+
def _call_handler(self, handler: Callable, params: Dict[str, Any]) -> Any:
|
|
251
|
+
"""调用处理函数
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
handler: 处理函数
|
|
255
|
+
params: 参数字典
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
执行结果
|
|
259
|
+
"""
|
|
260
|
+
import inspect
|
|
261
|
+
sig = inspect.signature(handler)
|
|
262
|
+
|
|
263
|
+
# 检查是否接受 **kwargs
|
|
264
|
+
has_var_keyword = any(
|
|
265
|
+
p.kind == inspect.Parameter.VAR_KEYWORD
|
|
266
|
+
for p in sig.parameters.values()
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if has_var_keyword:
|
|
270
|
+
return handler(**params)
|
|
271
|
+
|
|
272
|
+
# 过滤参数
|
|
273
|
+
valid_params = {
|
|
274
|
+
k: v for k, v in params.items()
|
|
275
|
+
if k in sig.parameters
|
|
276
|
+
}
|
|
277
|
+
return handler(**valid_params)
|
|
278
|
+
|
|
279
|
+
def get_available_keywords(self) -> List[str]:
|
|
280
|
+
"""获取所有可用关键字
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List[str]: 关键字列表
|
|
284
|
+
"""
|
|
285
|
+
keywords = []
|
|
286
|
+
|
|
287
|
+
# 核心关键字
|
|
288
|
+
if self._keyword_test:
|
|
289
|
+
for name in dir(self._keyword_test):
|
|
290
|
+
if not name.startswith('_') and callable(getattr(self._keyword_test, name)):
|
|
291
|
+
keywords.append(name)
|
|
292
|
+
|
|
293
|
+
# 插件关键字
|
|
294
|
+
keywords.extend(self._plugin_keywords.keys())
|
|
295
|
+
|
|
296
|
+
return sorted(set(keywords))
|
|
297
|
+
|
|
298
|
+
def has_keyword(self, keyword: str) -> bool:
|
|
299
|
+
"""检查关键字是否存在
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
keyword: 关键字名称
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
bool: 是否存在
|
|
306
|
+
"""
|
|
307
|
+
if keyword in self._plugin_keywords:
|
|
308
|
+
return True
|
|
309
|
+
if self._keyword_test and hasattr(self._keyword_test, keyword):
|
|
310
|
+
return True
|
|
311
|
+
return False
|