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,180 @@
1
+ """插件基类"""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Any, Callable, List, Optional
5
+ from playwright.sync_api import Page
6
+
7
+ from ..reference import MODULEDATA, INFO
8
+
9
+
10
+ class PluginBase(ABC):
11
+ """插件基类
12
+
13
+ 所有插件必须继承此类并实现必要方法。
14
+ """
15
+
16
+ # 插件元信息
17
+ name: str = "base_plugin"
18
+ version: str = "1.0.0"
19
+ description: str = "Base plugin"
20
+ author: str = ""
21
+
22
+ def __init__(self, page: Page = None):
23
+ """初始化插件
24
+
25
+ Args:
26
+ page: Playwright Page 实例
27
+ """
28
+ self._page = page
29
+ self._keywords: Dict[str, Callable] = {}
30
+ self._element_data: Dict[str, Any] = {}
31
+ self._config: Dict[str, Any] = {}
32
+ self._initialized = False
33
+
34
+ @abstractmethod
35
+ def setup(self) -> None:
36
+ """插件初始化
37
+
38
+ 子类必须实现此方法,用于:
39
+ - 注册关键字
40
+ - 加载元素数据
41
+ - 初始化配置
42
+ """
43
+ pass
44
+
45
+ def teardown(self) -> None:
46
+ """插件清理
47
+
48
+ 子类可选实现,用于清理资源。
49
+ """
50
+ pass
51
+
52
+ def register_keyword(self, name: str, handler: Callable) -> None:
53
+ """注册关键字
54
+
55
+ Args:
56
+ name: 关键字名称
57
+ handler: 处理函数
58
+ """
59
+ self._keywords[name] = handler
60
+ INFO(f"[{self.name}] 注册关键字: {name}")
61
+
62
+ def register_keywords(self, keywords: Dict[str, Callable]) -> None:
63
+ """批量注册关键字
64
+
65
+ Args:
66
+ keywords: 关键字字典
67
+ """
68
+ for name, handler in keywords.items():
69
+ self.register_keyword(name, handler)
70
+
71
+ def load_element_data(self, data: Dict[str, Any]) -> None:
72
+ """加载元素数据
73
+
74
+ Args:
75
+ data: 元素数据字典
76
+ """
77
+ self._element_data.update(data)
78
+ INFO(f"[{self.name}] 加载元素数据: {len(data)} 个模块")
79
+
80
+ def load_element_data_from_file(self, file_path: str) -> None:
81
+ """从文件加载元素数据
82
+
83
+ Args:
84
+ file_path: YAML 文件路径
85
+ """
86
+ try:
87
+ import yaml
88
+ from pathlib import Path
89
+
90
+ path = Path(file_path)
91
+ if not path.exists():
92
+ INFO(f"[{self.name}] 元素数据文件不存在: {file_path}", "WARNING")
93
+ return
94
+
95
+ with open(path, 'r', encoding='utf-8') as f:
96
+ data = yaml.safe_load(f) or {}
97
+ self.load_element_data(data)
98
+
99
+ except ImportError:
100
+ INFO(f"[{self.name}] PyYAML 未安装,无法加载 YAML 文件", "WARNING")
101
+ except Exception as e:
102
+ INFO(f"[{self.name}] 加载元素数据失败: {e}", "ERROR")
103
+
104
+ def load_config(self, config: Dict[str, Any]) -> None:
105
+ """加载配置
106
+
107
+ Args:
108
+ config: 配置字典
109
+ """
110
+ self._config.update(config)
111
+
112
+ def load_config_from_file(self, file_path: str) -> None:
113
+ """从 INI 文件加载配置
114
+
115
+ Args:
116
+ file_path: INI 文件路径
117
+ """
118
+ try:
119
+ import configparser
120
+ from pathlib import Path
121
+
122
+ path = Path(file_path)
123
+ if not path.exists():
124
+ INFO(f"[{self.name}] 配置文件不存在: {file_path}", "WARNING")
125
+ return
126
+
127
+ config = configparser.ConfigParser()
128
+ config.read(str(path), encoding='utf-8')
129
+
130
+ for section in config.sections():
131
+ self._config[section] = dict(config.items(section))
132
+
133
+ INFO(f"[{self.name}] 加载配置: {list(self._config.keys())}")
134
+
135
+ except Exception as e:
136
+ INFO(f"[{self.name}] 加载配置失败: {e}", "ERROR")
137
+
138
+ @property
139
+ def keywords(self) -> Dict[str, Callable]:
140
+ """获取所有关键字"""
141
+ return self._keywords.copy()
142
+
143
+ @property
144
+ def element_data(self) -> Dict[str, Any]:
145
+ """获取元素数据"""
146
+ return self._element_data.copy()
147
+
148
+ @property
149
+ def config(self) -> Dict[str, Any]:
150
+ """获取配置"""
151
+ return self._config.copy()
152
+
153
+ @property
154
+ def page(self) -> Optional[Page]:
155
+ """获取 Page 实例"""
156
+ return self._page
157
+
158
+ def set_page(self, page: Page) -> None:
159
+ """设置 Page 实例
160
+
161
+ Args:
162
+ page: Playwright Page 实例
163
+ """
164
+ self._page = page
165
+
166
+ def initialize(self) -> None:
167
+ """初始化插件(内部调用)"""
168
+ if not self._initialized:
169
+ self.setup()
170
+ self._initialized = True
171
+
172
+ # 存储到全局
173
+ MODULEDATA[self.name] = {
174
+ 'keywords': self._keywords,
175
+ 'element_data': self._element_data,
176
+ 'config': self._config,
177
+ }
178
+
179
+ def __repr__(self) -> str:
180
+ return f"<Plugin {self.name} v{self.version}>"
@@ -0,0 +1,260 @@
1
+ """动态插件加载器"""
2
+
3
+ import importlib
4
+ import importlib.util
5
+ import sys
6
+ from typing import Dict, List, Optional, Type
7
+ from pathlib import Path
8
+
9
+ from playwright.sync_api import Page
10
+
11
+ from .plugin_base import PluginBase
12
+ from ..reference import MODULEDATA, INFO
13
+
14
+
15
+ class PluginLoader:
16
+ """动态插件加载器
17
+
18
+ 从指定目录或模块加载插件。
19
+ """
20
+
21
+ def __init__(self, page: Page = None):
22
+ """初始化加载器
23
+
24
+ Args:
25
+ page: Playwright Page 实例
26
+ """
27
+ self._page = page
28
+ self._plugins: Dict[str, PluginBase] = {}
29
+ self._plugin_paths: List[Path] = []
30
+
31
+ def add_plugin_path(self, path: str) -> None:
32
+ """添加插件搜索路径
33
+
34
+ Args:
35
+ path: 插件目录路径
36
+ """
37
+ plugin_path = Path(path)
38
+ if plugin_path.exists() and plugin_path.is_dir():
39
+ self._plugin_paths.append(plugin_path)
40
+ INFO(f"添加插件路径: {plugin_path}")
41
+
42
+ def load_from_path(self, path: str) -> Optional[PluginBase]:
43
+ """从路径加载单个插件
44
+
45
+ Args:
46
+ path: 插件目录或模块路径
47
+
48
+ Returns:
49
+ PluginBase: 加载的插件实例
50
+ """
51
+ plugin_path = Path(path)
52
+
53
+ if plugin_path.is_dir():
54
+ # 目录形式的插件
55
+ return self._load_from_directory(plugin_path)
56
+ elif plugin_path.is_file() and plugin_path.suffix == '.py':
57
+ # 单文件插件
58
+ return self._load_from_file(plugin_path)
59
+
60
+ return None
61
+
62
+ def _load_from_directory(self, directory: Path) -> Optional[PluginBase]:
63
+ """从目录加载插件
64
+
65
+ 目录结构:
66
+ plugin_name/
67
+ ├── __init__.py
68
+ ├── plugin_name_plugin.py (包含插件类)
69
+ ├── my.ini (可选配置)
70
+ └── elementData/
71
+ └── elementData.yaml (可选元素数据)
72
+
73
+ Args:
74
+ directory: 插件目录
75
+
76
+ Returns:
77
+ PluginBase: 插件实例
78
+ """
79
+ plugin_name = directory.name
80
+
81
+ # 查找插件主文件
82
+ plugin_file = directory / f"{plugin_name}_plugin.py"
83
+ if not plugin_file.exists():
84
+ # 尝试其他命名
85
+ plugin_files = list(directory.glob("*_plugin.py"))
86
+ if plugin_files:
87
+ plugin_file = plugin_files[0]
88
+ else:
89
+ INFO(f"未找到插件主文件: {directory}", "WARNING")
90
+ return None
91
+
92
+ # 加载模块
93
+ spec = importlib.util.spec_from_file_location(plugin_name, plugin_file)
94
+ if spec is None or spec.loader is None:
95
+ return None
96
+
97
+ module = importlib.util.module_from_spec(spec)
98
+ sys.modules[plugin_name] = module
99
+ spec.loader.exec_module(module)
100
+
101
+ # 查找插件类
102
+ plugin_class = self._find_plugin_class(module)
103
+ if not plugin_class:
104
+ INFO(f"未找到 PluginBase 子类: {plugin_file}", "WARNING")
105
+ return None
106
+
107
+ # 创建实例
108
+ plugin = plugin_class(self._page)
109
+
110
+ # 加载配置
111
+ config_file = directory / "my.ini"
112
+ if config_file.exists():
113
+ plugin.load_config_from_file(str(config_file))
114
+
115
+ # 加载元素数据
116
+ element_data_file = directory / "elementData" / "elementData.yaml"
117
+ if element_data_file.exists():
118
+ plugin.load_element_data_from_file(str(element_data_file))
119
+
120
+ # 初始化
121
+ plugin.initialize()
122
+
123
+ self._plugins[plugin.name] = plugin
124
+ INFO(f"加载插件: {plugin}")
125
+
126
+ return plugin
127
+
128
+ def _load_from_file(self, file_path: Path) -> Optional[PluginBase]:
129
+ """从单文件加载插件
130
+
131
+ Args:
132
+ file_path: 插件文件路径
133
+
134
+ Returns:
135
+ PluginBase: 插件实例
136
+ """
137
+ plugin_name = file_path.stem
138
+
139
+ spec = importlib.util.spec_from_file_location(plugin_name, file_path)
140
+ if spec is None or spec.loader is None:
141
+ return None
142
+
143
+ module = importlib.util.module_from_spec(spec)
144
+ sys.modules[plugin_name] = module
145
+ spec.loader.exec_module(module)
146
+
147
+ plugin_class = self._find_plugin_class(module)
148
+ if not plugin_class:
149
+ INFO(f"未找到 PluginBase 子类: {file_path}", "WARNING")
150
+ return None
151
+
152
+ plugin = plugin_class(self._page)
153
+ plugin.initialize()
154
+
155
+ self._plugins[plugin.name] = plugin
156
+ INFO(f"加载插件: {plugin}")
157
+
158
+ return plugin
159
+
160
+ def _find_plugin_class(self, module) -> Optional[Type[PluginBase]]:
161
+ """在模块中查找 PluginBase 子类
162
+
163
+ Args:
164
+ module: Python 模块
165
+
166
+ Returns:
167
+ PluginBase 子类
168
+ """
169
+ for name in dir(module):
170
+ obj = getattr(module, name)
171
+ if (
172
+ isinstance(obj, type) and
173
+ issubclass(obj, PluginBase) and
174
+ obj is not PluginBase
175
+ ):
176
+ return obj
177
+ return None
178
+
179
+ def load_builtin_plugins(self) -> None:
180
+ """加载内置插件"""
181
+ # 加载 Element Plus 插件
182
+ builtin_path = Path(__file__).parent / "element_plus_plugin"
183
+ if builtin_path.exists():
184
+ self.load_from_path(str(builtin_path))
185
+
186
+ def discover_plugins(self) -> List[PluginBase]:
187
+ """从所有插件路径发现并加载插件
188
+
189
+ Returns:
190
+ List[PluginBase]: 加载的插件列表
191
+ """
192
+ plugins = []
193
+
194
+ for plugin_path in self._plugin_paths:
195
+ for item in plugin_path.iterdir():
196
+ if item.is_dir() and not item.name.startswith('_'):
197
+ plugin = self._load_from_directory(item)
198
+ if plugin:
199
+ plugins.append(plugin)
200
+ elif item.is_file() and item.suffix == '.py' and not item.name.startswith('_'):
201
+ plugin = self._load_from_file(item)
202
+ if plugin:
203
+ plugins.append(plugin)
204
+
205
+ return plugins
206
+
207
+ def get_plugin(self, name: str) -> Optional[PluginBase]:
208
+ """获取插件
209
+
210
+ Args:
211
+ name: 插件名称
212
+
213
+ Returns:
214
+ PluginBase: 插件实例
215
+ """
216
+ return self._plugins.get(name)
217
+
218
+ @property
219
+ def plugins(self) -> Dict[str, PluginBase]:
220
+ """获取所有加载的插件"""
221
+ return self._plugins.copy()
222
+
223
+ def get_all_keywords(self) -> Dict[str, callable]:
224
+ """获取所有插件的关键字
225
+
226
+ Returns:
227
+ Dict[str, Callable]: 关键字字典
228
+ """
229
+ keywords = {}
230
+ for plugin in self._plugins.values():
231
+ keywords.update(plugin.keywords)
232
+ return keywords
233
+
234
+ def get_all_element_data(self) -> Dict[str, any]:
235
+ """获取所有插件的元素数据
236
+
237
+ Returns:
238
+ Dict[str, Any]: 元素数据字典
239
+ """
240
+ element_data = {}
241
+ for plugin in self._plugins.values():
242
+ element_data.update(plugin.element_data)
243
+ return element_data
244
+
245
+ def set_page(self, page: Page) -> None:
246
+ """设置 Page 实例
247
+
248
+ Args:
249
+ page: Playwright Page 实例
250
+ """
251
+ self._page = page
252
+ for plugin in self._plugins.values():
253
+ plugin.set_page(page)
254
+
255
+ def unload_all(self) -> None:
256
+ """卸载所有插件"""
257
+ for plugin in self._plugins.values():
258
+ plugin.teardown()
259
+ self._plugins.clear()
260
+ INFO("已卸载所有插件")
kdtest_pw/product.py ADDED
@@ -0,0 +1,5 @@
1
+ """KDTest-Playwright 版本信息"""
2
+
3
+ __version__ = "2.0.0"
4
+ __author__ = "KDTest Team"
5
+ __description__ = "KDTest Playwright Edition - 关键字驱动测试框架"
kdtest_pw/reference.py ADDED
@@ -0,0 +1,99 @@
1
+ """全局存储模块
2
+
3
+ 提供全局变量存储,用于在模块间共享数据。
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+
8
+ # 全局存储 - 核心组件引用
9
+ GSTORE: Dict[str, Any] = {
10
+ 'browser': None, # Browser实例
11
+ 'context': None, # BrowserContext实例
12
+ 'page': None, # 当前Page实例
13
+ 'keyWord': None, # KeyWordTest实例
14
+ }
15
+
16
+ # 全局系统数据存储
17
+ GSDSTORE: Dict[str, Any] = {
18
+ 'START': {}, # 启动参数
19
+ 'WORKPATH': {}, # 工作路径
20
+ }
21
+
22
+ # 模块数据存储 - 插件数据
23
+ MODULEDATA: Dict[str, Any] = {}
24
+
25
+ # 私有数据存储
26
+ PRIVATEDATA: Dict[str, Any] = {
27
+ 'PAGES': [], # 多页面列表
28
+ 'ELEVALUE': {}, # 元素值缓存
29
+ 'CURRENT_CASE': None, # 当前用例名称
30
+ }
31
+
32
+ # 用例统计数据
33
+ CASESDATA: Dict[str, int] = {
34
+ 'total': 0,
35
+ 'passed': 0,
36
+ 'failed': 0,
37
+ 'skipped': 0,
38
+ }
39
+
40
+
41
+ def INFO(message: str, level: str = "INFO") -> None:
42
+ """统一日志输出
43
+
44
+ Args:
45
+ message: 日志消息
46
+ level: 日志级别 (INFO, WARNING, ERROR, DEBUG)
47
+ """
48
+ print(f"[{level}] {message}")
49
+
50
+
51
+ def get_global(key: str, default: Any = None) -> Any:
52
+ """获取全局存储的值
53
+
54
+ Args:
55
+ key: 键名
56
+ default: 默认值
57
+
58
+ Returns:
59
+ 存储的值或默认值
60
+ """
61
+ return GSTORE.get(key, default)
62
+
63
+
64
+ def set_global(key: str, value: Any) -> None:
65
+ """设置全局存储的值
66
+
67
+ Args:
68
+ key: 键名
69
+ value: 值
70
+ """
71
+ GSTORE[key] = value
72
+
73
+
74
+ def get_element_value(key: str, default: Any = None) -> Any:
75
+ """获取缓存的元素值
76
+
77
+ Args:
78
+ key: 键名
79
+ default: 默认值
80
+
81
+ Returns:
82
+ 缓存的元素值或默认值
83
+ """
84
+ return PRIVATEDATA['ELEVALUE'].get(key, default)
85
+
86
+
87
+ def set_element_value(key: str, value: Any) -> None:
88
+ """设置元素值缓存
89
+
90
+ Args:
91
+ key: 键名
92
+ value: 值
93
+ """
94
+ PRIVATEDATA['ELEVALUE'][key] = value
95
+
96
+
97
+ def clear_element_values() -> None:
98
+ """清除所有元素值缓存"""
99
+ PRIVATEDATA['ELEVALUE'].clear()
@@ -0,0 +1,13 @@
1
+ """工具模块"""
2
+
3
+ from .decorator import retry, timeout, catch_exception
4
+ from .built_in_function import BuiltInFunction
5
+ from .public_script import PublicScript
6
+
7
+ __all__ = [
8
+ 'retry',
9
+ 'timeout',
10
+ 'catch_exception',
11
+ 'BuiltInFunction',
12
+ 'PublicScript',
13
+ ]