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.
Files changed (57) hide show
  1. kdtest_pw/__init__.py +50 -0
  2. kdtest_pw/action/__init__.py +7 -0
  3. kdtest_pw/action/base_keyword.py +292 -0
  4. kdtest_pw/action/element_plus/__init__.py +23 -0
  5. kdtest_pw/action/element_plus/el_cascader.py +263 -0
  6. kdtest_pw/action/element_plus/el_datepicker.py +324 -0
  7. kdtest_pw/action/element_plus/el_dialog.py +317 -0
  8. kdtest_pw/action/element_plus/el_form.py +443 -0
  9. kdtest_pw/action/element_plus/el_menu.py +456 -0
  10. kdtest_pw/action/element_plus/el_select.py +268 -0
  11. kdtest_pw/action/element_plus/el_table.py +442 -0
  12. kdtest_pw/action/element_plus/el_tree.py +364 -0
  13. kdtest_pw/action/element_plus/el_upload.py +313 -0
  14. kdtest_pw/action/key_retrieval.py +311 -0
  15. kdtest_pw/action/page_action.py +1129 -0
  16. kdtest_pw/api/__init__.py +6 -0
  17. kdtest_pw/api/api_keyword.py +251 -0
  18. kdtest_pw/api/request_handler.py +232 -0
  19. kdtest_pw/cases/__init__.py +6 -0
  20. kdtest_pw/cases/case_collector.py +182 -0
  21. kdtest_pw/cases/case_executor.py +359 -0
  22. kdtest_pw/cases/read/__init__.py +6 -0
  23. kdtest_pw/cases/read/cell_handler.py +305 -0
  24. kdtest_pw/cases/read/excel_reader.py +223 -0
  25. kdtest_pw/cli/__init__.py +5 -0
  26. kdtest_pw/cli/run.py +318 -0
  27. kdtest_pw/common.py +106 -0
  28. kdtest_pw/core/__init__.py +7 -0
  29. kdtest_pw/core/browser_manager.py +196 -0
  30. kdtest_pw/core/config_loader.py +235 -0
  31. kdtest_pw/core/page_context.py +228 -0
  32. kdtest_pw/data/__init__.py +5 -0
  33. kdtest_pw/data/init_data.py +105 -0
  34. kdtest_pw/data/static/elementData.yaml +59 -0
  35. kdtest_pw/data/static/parameters.json +24 -0
  36. kdtest_pw/plugins/__init__.py +6 -0
  37. kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
  38. kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
  39. kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
  40. kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
  41. kdtest_pw/plugins/plugin_base.py +180 -0
  42. kdtest_pw/plugins/plugin_loader.py +260 -0
  43. kdtest_pw/product.py +5 -0
  44. kdtest_pw/reference.py +99 -0
  45. kdtest_pw/utils/__init__.py +13 -0
  46. kdtest_pw/utils/built_in_function.py +376 -0
  47. kdtest_pw/utils/decorator.py +211 -0
  48. kdtest_pw/utils/log/__init__.py +6 -0
  49. kdtest_pw/utils/log/html_report.py +336 -0
  50. kdtest_pw/utils/log/logger.py +123 -0
  51. kdtest_pw/utils/public_script.py +366 -0
  52. kdtest_pw-2.0.0.dist-info/METADATA +169 -0
  53. kdtest_pw-2.0.0.dist-info/RECORD +57 -0
  54. kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
  55. kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
  56. kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
  57. kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,456 @@
1
+ """菜单导航关键字 - 支持多级菜单结构"""
2
+
3
+ import json
4
+ from playwright.sync_api import Page, Locator
5
+ from typing import Optional, Dict, Any, List
6
+ from pathlib import Path
7
+
8
+ from ..base_keyword import BaseKeyword
9
+ from ...reference import INFO, MODULEDATA, set_element_value
10
+
11
+
12
+ class ElMenuKeyword(BaseKeyword):
13
+ """多级菜单导航关键字
14
+
15
+ 支持 modularInformation.json 格式的菜单配置:
16
+ {
17
+ "一级菜单": {
18
+ "selfLocatin": ["xpath", "定位表达式"],
19
+ "subordinate": {
20
+ "二级菜单": {
21
+ "iframe": "iframe名称",
22
+ "locatingNodes": ["xpath", "定位表达式"],
23
+ "submodule": {...}, // 三级菜单
24
+ "tagPage": [...] // 页签
25
+ }
26
+ }
27
+ }
28
+ }
29
+ """
30
+
31
+ def __init__(self, page: Page):
32
+ super().__init__(page)
33
+ self._menu_data: Dict[str, Any] = {}
34
+ self._current_iframe: Optional[str] = None
35
+
36
+ def load_menu_data(self, file_path: str = None, *, content: str = None) -> None:
37
+ """加载菜单配置文件
38
+
39
+ Args:
40
+ file_path: modularInformation.json 文件路径
41
+ content: 文件路径 (关键字调用时使用)
42
+ """
43
+ # 支持两种调用方式
44
+ path_str = content or file_path
45
+ if not path_str:
46
+ INFO("未指定菜单配置文件路径", "ERROR")
47
+ return
48
+
49
+ path = Path(path_str)
50
+ if not path.exists():
51
+ INFO(f"菜单配置文件不存在: {path_str}", "ERROR")
52
+ return
53
+
54
+ with open(path, 'r', encoding='utf-8') as f:
55
+ self._menu_data = json.load(f)
56
+
57
+ MODULEDATA['menuData'] = self._menu_data
58
+ INFO(f"加载菜单配置: {len(self._menu_data)} 个一级菜单")
59
+
60
+ def set_menu_data(self, menu_data: Dict[str, Any]) -> None:
61
+ """直接设置菜单数据
62
+
63
+ Args:
64
+ menu_data: 菜单数据字典
65
+ """
66
+ self._menu_data = menu_data
67
+ MODULEDATA['menuData'] = self._menu_data
68
+
69
+ # ==================== 菜单导航关键字 ====================
70
+
71
+ def menu_navigate(
72
+ self,
73
+ targeting: str = None,
74
+ element: str = None,
75
+ index: Optional[int] = None,
76
+ *,
77
+ content: str
78
+ ) -> None:
79
+ """导航到指定菜单
80
+
81
+ 支持多种格式:
82
+ - "一级菜单" - 只点击一级菜单
83
+ - "一级菜单/二级菜单" - 导航到二级菜单
84
+ - "一级菜单/二级菜单/三级菜单" - 导航到三级菜单
85
+ - "一级菜单/二级菜单#页签索引" - 导航到二级菜单并点击页签
86
+
87
+ Args:
88
+ targeting: 未使用
89
+ element: 未使用
90
+ index: 未使用
91
+ content: 菜单路径
92
+ """
93
+ menu_path = str(content)
94
+ INFO(f"菜单导航: {menu_path}")
95
+
96
+ # 解析页签
97
+ tag_index = None
98
+ if '#' in menu_path:
99
+ menu_path, tag_str = menu_path.rsplit('#', 1)
100
+ tag_index = int(tag_str)
101
+
102
+ # 解析菜单层级
103
+ levels = [level.strip() for level in menu_path.split('/')]
104
+
105
+ if len(levels) >= 1:
106
+ # 点击一级菜单
107
+ self._click_first_level_menu(levels[0])
108
+
109
+ if len(levels) >= 2:
110
+ # 点击二级菜单
111
+ self._click_second_level_menu(levels[0], levels[1])
112
+
113
+ if len(levels) >= 3:
114
+ # 点击三级菜单
115
+ self._click_third_level_menu(levels[0], levels[1], levels[2])
116
+
117
+ # 点击页签
118
+ if tag_index is not None and len(levels) >= 2:
119
+ self._click_tag_page(levels[0], levels[1], tag_index)
120
+
121
+ def menu_click_first(self, *, content: str) -> None:
122
+ """点击一级菜单
123
+
124
+ Args:
125
+ content: 一级菜单名称
126
+ """
127
+ self._click_first_level_menu(content)
128
+
129
+ def menu_click_second(self, *, content: str) -> None:
130
+ """点击二级菜单
131
+
132
+ Args:
133
+ content: 格式 "一级菜单/二级菜单"
134
+ """
135
+ levels = [level.strip() for level in content.split('/')]
136
+ if len(levels) < 2:
137
+ INFO(f"二级菜单格式错误: {content}", "ERROR")
138
+ return
139
+
140
+ self._click_first_level_menu(levels[0])
141
+ self._click_second_level_menu(levels[0], levels[1])
142
+
143
+ def menu_click_third(self, *, content: str) -> None:
144
+ """点击三级菜单
145
+
146
+ Args:
147
+ content: 格式 "一级菜单/二级菜单/三级菜单"
148
+ """
149
+ levels = [level.strip() for level in content.split('/')]
150
+ if len(levels) < 3:
151
+ INFO(f"三级菜单格式错误: {content}", "ERROR")
152
+ return
153
+
154
+ self._click_first_level_menu(levels[0])
155
+ self._click_second_level_menu(levels[0], levels[1])
156
+ self._click_third_level_menu(levels[0], levels[1], levels[2])
157
+
158
+ def menu_click_tag(self, *, content: str, tag_index: int = 0) -> None:
159
+ """点击页签
160
+
161
+ Args:
162
+ content: 格式 "一级菜单/二级菜单"
163
+ tag_index: 页签索引 (从0开始)
164
+ """
165
+ levels = [level.strip() for level in content.split('/')]
166
+ if len(levels) < 2:
167
+ INFO(f"菜单格式错误: {content}", "ERROR")
168
+ return
169
+
170
+ self._click_tag_page(levels[0], levels[1], tag_index)
171
+
172
+ def menu_switch_iframe(self, *, content: str) -> None:
173
+ """切换到菜单对应的 iframe
174
+
175
+ Args:
176
+ content: 格式 "一级菜单/二级菜单"
177
+ """
178
+ levels = [level.strip() for level in content.split('/')]
179
+ if len(levels) < 2:
180
+ INFO(f"菜单格式错误: {content}", "ERROR")
181
+ return
182
+
183
+ first_menu = levels[0]
184
+ second_menu = levels[1]
185
+
186
+ menu_info = self._get_second_menu_info(first_menu, second_menu)
187
+ if not menu_info:
188
+ return
189
+
190
+ iframe_name = menu_info.get('iframe', '')
191
+ if iframe_name:
192
+ self._switch_to_iframe(iframe_name)
193
+ else:
194
+ INFO(f"菜单 {content} 没有 iframe 配置")
195
+
196
+ def menu_exit_iframe(self) -> None:
197
+ """退出当前 iframe,返回主文档"""
198
+ self.frame_default()
199
+ self._current_iframe = None
200
+ INFO("退出 iframe,返回主文档")
201
+
202
+ # ==================== 内部方法 ====================
203
+
204
+ def _click_first_level_menu(self, menu_name: str) -> None:
205
+ """点击一级菜单
206
+
207
+ Args:
208
+ menu_name: 一级菜单名称
209
+ """
210
+ if menu_name not in self._menu_data:
211
+ INFO(f"一级菜单不存在: {menu_name}", "ERROR")
212
+ return
213
+
214
+ menu_config = self._menu_data[menu_name]
215
+ locator_info = menu_config.get('selfLocatin', [])
216
+
217
+ if len(locator_info) >= 2:
218
+ targeting = locator_info[0]
219
+ element = locator_info[1]
220
+
221
+ # 确保在主文档中
222
+ self.frame_default()
223
+
224
+ loc = self.locator(targeting, element)
225
+ loc.click()
226
+ self.page.wait_for_timeout(300)
227
+ INFO(f"点击一级菜单: {menu_name}")
228
+ else:
229
+ INFO(f"一级菜单 {menu_name} 缺少定位信息", "ERROR")
230
+
231
+ def _click_second_level_menu(self, first_menu: str, second_menu: str) -> None:
232
+ """点击二级菜单
233
+
234
+ Args:
235
+ first_menu: 一级菜单名称
236
+ second_menu: 二级菜单名称
237
+ """
238
+ menu_info = self._get_second_menu_info(first_menu, second_menu)
239
+ if not menu_info:
240
+ return
241
+
242
+ locator_info = menu_info.get('locatingNodes', [])
243
+
244
+ if len(locator_info) >= 2:
245
+ targeting = locator_info[0]
246
+ element = locator_info[1]
247
+
248
+ # 确保在主文档中(二级菜单通常在主文档)
249
+ self.frame_default()
250
+
251
+ loc = self.locator(targeting, element)
252
+ loc.click()
253
+ self.page.wait_for_timeout(500)
254
+ INFO(f"点击二级菜单: {first_menu}/{second_menu}")
255
+
256
+ # 自动切换到 iframe(如果配置了)
257
+ iframe_name = menu_info.get('iframe', '')
258
+ if iframe_name:
259
+ self._switch_to_iframe(iframe_name)
260
+ else:
261
+ INFO(f"二级菜单 {first_menu}/{second_menu} 缺少定位信息", "ERROR")
262
+
263
+ def _click_third_level_menu(self, first_menu: str, second_menu: str, third_menu: str) -> None:
264
+ """点击三级菜单
265
+
266
+ Args:
267
+ first_menu: 一级菜单名称
268
+ second_menu: 二级菜单名称
269
+ third_menu: 三级菜单名称
270
+ """
271
+ menu_info = self._get_second_menu_info(first_menu, second_menu)
272
+ if not menu_info:
273
+ return
274
+
275
+ submodule = menu_info.get('submodule')
276
+ if not submodule or third_menu not in submodule:
277
+ INFO(f"三级菜单不存在: {first_menu}/{second_menu}/{third_menu}", "ERROR")
278
+ return
279
+
280
+ third_menu_info = submodule[third_menu]
281
+ locator_info = third_menu_info.get('locatingNodes', [])
282
+
283
+ if len(locator_info) >= 2:
284
+ targeting = locator_info[0]
285
+ element = locator_info[1]
286
+
287
+ loc = self.locator(targeting, element)
288
+ loc.click()
289
+ self.page.wait_for_timeout(300)
290
+ INFO(f"点击三级菜单: {first_menu}/{second_menu}/{third_menu}")
291
+
292
+ # 三级菜单可能也有自己的 iframe
293
+ iframe_name = third_menu_info.get('iframe', '')
294
+ if iframe_name:
295
+ self._switch_to_iframe(iframe_name)
296
+ else:
297
+ INFO(f"三级菜单 {third_menu} 缺少定位信息", "ERROR")
298
+
299
+ def _click_tag_page(self, first_menu: str, second_menu: str, tag_index: int) -> None:
300
+ """点击页签
301
+
302
+ Args:
303
+ first_menu: 一级菜单名称
304
+ second_menu: 二级菜单名称
305
+ tag_index: 页签索引
306
+ """
307
+ menu_info = self._get_second_menu_info(first_menu, second_menu)
308
+ if not menu_info:
309
+ return
310
+
311
+ tag_pages = menu_info.get('tagPage')
312
+ if not tag_pages:
313
+ INFO(f"菜单 {first_menu}/{second_menu} 没有页签配置", "WARNING")
314
+ return
315
+
316
+ if tag_index >= len(tag_pages):
317
+ INFO(f"页签索引超出范围: {tag_index} (共 {len(tag_pages)} 个页签)", "ERROR")
318
+ return
319
+
320
+ locator_info = tag_pages[tag_index]
321
+ if len(locator_info) >= 2:
322
+ targeting = locator_info[0]
323
+ element = locator_info[1]
324
+
325
+ loc = self.locator(targeting, element)
326
+ loc.click()
327
+ self.page.wait_for_timeout(300)
328
+ INFO(f"点击页签 [{tag_index}]: {first_menu}/{second_menu}")
329
+ else:
330
+ INFO(f"页签 {tag_index} 缺少定位信息", "ERROR")
331
+
332
+ def _get_second_menu_info(self, first_menu: str, second_menu: str) -> Optional[Dict[str, Any]]:
333
+ """获取二级菜单配置
334
+
335
+ Args:
336
+ first_menu: 一级菜单名称
337
+ second_menu: 二级菜单名称
338
+
339
+ Returns:
340
+ 二级菜单配置字典
341
+ """
342
+ if first_menu not in self._menu_data:
343
+ INFO(f"一级菜单不存在: {first_menu}", "ERROR")
344
+ return None
345
+
346
+ subordinate = self._menu_data[first_menu].get('subordinate', {})
347
+ if second_menu not in subordinate:
348
+ INFO(f"二级菜单不存在: {first_menu}/{second_menu}", "ERROR")
349
+ return None
350
+
351
+ return subordinate[second_menu]
352
+
353
+ def _switch_to_iframe(self, iframe_name: str) -> None:
354
+ """切换到指定 iframe
355
+
356
+ Args:
357
+ iframe_name: iframe 名称或 ID
358
+ """
359
+ if not iframe_name:
360
+ return
361
+
362
+ # 先回到主文档
363
+ self.frame_default()
364
+
365
+ # 切换到目标 iframe
366
+ try:
367
+ # 尝试通过 name 或 id 定位
368
+ frame_locator = self.page.frame_locator(f'iframe[name="{iframe_name}"], iframe#{iframe_name}, #{iframe_name}')
369
+ self._current_frame = frame_locator
370
+ self._current_iframe = iframe_name
371
+ INFO(f"切换到 iframe: {iframe_name}")
372
+ except Exception as e:
373
+ INFO(f"切换 iframe 失败: {iframe_name} - {e}", "ERROR")
374
+
375
+ # ==================== 辅助方法 ====================
376
+
377
+ def menu_get_first_menus(self) -> List[str]:
378
+ """获取所有一级菜单名称
379
+
380
+ Returns:
381
+ List[str]: 一级菜单名称列表
382
+ """
383
+ return list(self._menu_data.keys())
384
+
385
+ def menu_get_second_menus(self, *, content: str) -> List[str]:
386
+ """获取指定一级菜单下的所有二级菜单
387
+
388
+ Args:
389
+ content: 一级菜单名称
390
+
391
+ Returns:
392
+ List[str]: 二级菜单名称列表
393
+ """
394
+ if content not in self._menu_data:
395
+ return []
396
+
397
+ subordinate = self._menu_data[content].get('subordinate', {})
398
+ return list(subordinate.keys())
399
+
400
+ def menu_get_tag_count(self, *, content: str) -> int:
401
+ """获取菜单的页签数量
402
+
403
+ Args:
404
+ content: 格式 "一级菜单/二级菜单"
405
+
406
+ Returns:
407
+ int: 页签数量
408
+ """
409
+ levels = [level.strip() for level in content.split('/')]
410
+ if len(levels) < 2:
411
+ return 0
412
+
413
+ menu_info = self._get_second_menu_info(levels[0], levels[1])
414
+ if not menu_info:
415
+ return 0
416
+
417
+ tag_pages = menu_info.get('tagPage')
418
+ return len(tag_pages) if tag_pages else 0
419
+
420
+ def menu_has_iframe(self, *, content: str) -> bool:
421
+ """检查菜单是否有 iframe
422
+
423
+ Args:
424
+ content: 格式 "一级菜单/二级菜单"
425
+
426
+ Returns:
427
+ bool: 是否有 iframe
428
+ """
429
+ levels = [level.strip() for level in content.split('/')]
430
+ if len(levels) < 2:
431
+ return False
432
+
433
+ menu_info = self._get_second_menu_info(levels[0], levels[1])
434
+ if not menu_info:
435
+ return False
436
+
437
+ return bool(menu_info.get('iframe'))
438
+
439
+ def menu_get_iframe_name(self, *, content: str) -> str:
440
+ """获取菜单的 iframe 名称
441
+
442
+ Args:
443
+ content: 格式 "一级菜单/二级菜单"
444
+
445
+ Returns:
446
+ str: iframe 名称
447
+ """
448
+ levels = [level.strip() for level in content.split('/')]
449
+ if len(levels) < 2:
450
+ return ''
451
+
452
+ menu_info = self._get_second_menu_info(levels[0], levels[1])
453
+ if not menu_info:
454
+ return ''
455
+
456
+ return menu_info.get('iframe', '')