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,336 @@
1
+ """HTML 测试报告生成器"""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+
8
+ from ...cases.case_executor import CaseResult, StepResult
9
+ from ...reference import INFO
10
+
11
+
12
+ class HtmlReporter:
13
+ """HTML 测试报告生成器"""
14
+
15
+ def __init__(self, report_dir: str = 'result/report'):
16
+ """初始化报告生成器
17
+
18
+ Args:
19
+ report_dir: 报告输出目录
20
+ """
21
+ self._report_dir = Path(report_dir)
22
+ self._report_dir.mkdir(parents=True, exist_ok=True)
23
+
24
+ def generate(
25
+ self,
26
+ results: List[CaseResult],
27
+ title: str = 'KDTest 测试报告',
28
+ output_file: Optional[str] = None
29
+ ) -> str:
30
+ """生成 HTML 报告
31
+
32
+ Args:
33
+ results: 测试结果列表
34
+ title: 报告标题
35
+ output_file: 输出文件名
36
+
37
+ Returns:
38
+ str: 报告文件路径
39
+ """
40
+ # 生成文件名
41
+ if not output_file:
42
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
43
+ output_file = f'report_{timestamp}.html'
44
+
45
+ report_path = self._report_dir / output_file
46
+
47
+ # 统计数据
48
+ total = len(results)
49
+ passed = sum(1 for r in results if r.is_passed)
50
+ failed = sum(1 for r in results if r.is_failed)
51
+ skipped = total - passed - failed
52
+ duration = sum(r.duration for r in results)
53
+ pass_rate = (passed / total * 100) if total > 0 else 0
54
+
55
+ # 生成 HTML
56
+ html = self._generate_html(
57
+ title=title,
58
+ results=results,
59
+ total=total,
60
+ passed=passed,
61
+ failed=failed,
62
+ skipped=skipped,
63
+ duration=duration,
64
+ pass_rate=pass_rate,
65
+ )
66
+
67
+ # 写入文件
68
+ with open(report_path, 'w', encoding='utf-8') as f:
69
+ f.write(html)
70
+
71
+ INFO(f"报告已生成: {report_path}")
72
+ return str(report_path)
73
+
74
+ def _generate_html(
75
+ self,
76
+ title: str,
77
+ results: List[CaseResult],
78
+ total: int,
79
+ passed: int,
80
+ failed: int,
81
+ skipped: int,
82
+ duration: float,
83
+ pass_rate: float,
84
+ ) -> str:
85
+ """生成 HTML 内容"""
86
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
87
+
88
+ # 生成用例行
89
+ case_rows = []
90
+ for i, result in enumerate(results, 1):
91
+ status_class = 'passed' if result.is_passed else 'failed'
92
+ status_text = '通过' if result.is_passed else '失败'
93
+
94
+ steps_html = self._generate_steps_html(result.step_results)
95
+
96
+ case_rows.append(f'''
97
+ <tr class="case-row {status_class}" onclick="toggleSteps(this)">
98
+ <td>{i}</td>
99
+ <td>{result.case.id}</td>
100
+ <td>{result.case.name}</td>
101
+ <td>{result.case.sheet}</td>
102
+ <td class="status-{status_class}">{status_text}</td>
103
+ <td>{result.duration:.2f}s</td>
104
+ </tr>
105
+ <tr class="steps-row" style="display: none;">
106
+ <td colspan="6">
107
+ <table class="steps-table">
108
+ <thead>
109
+ <tr>
110
+ <th>步骤</th>
111
+ <th>关键字</th>
112
+ <th>描述</th>
113
+ <th>状态</th>
114
+ <th>耗时</th>
115
+ <th>错误</th>
116
+ </tr>
117
+ </thead>
118
+ <tbody>
119
+ {steps_html}
120
+ </tbody>
121
+ </table>
122
+ </td>
123
+ </tr>
124
+ ''')
125
+
126
+ cases_html = '\n'.join(case_rows)
127
+
128
+ return f'''<!DOCTYPE html>
129
+ <html lang="zh-CN">
130
+ <head>
131
+ <meta charset="UTF-8">
132
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
133
+ <title>{title}</title>
134
+ <style>
135
+ * {{
136
+ margin: 0;
137
+ padding: 0;
138
+ box-sizing: border-box;
139
+ }}
140
+ body {{
141
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
142
+ background-color: #f5f7fa;
143
+ color: #333;
144
+ line-height: 1.6;
145
+ }}
146
+ .container {{
147
+ max-width: 1400px;
148
+ margin: 0 auto;
149
+ padding: 20px;
150
+ }}
151
+ .header {{
152
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
153
+ color: white;
154
+ padding: 30px;
155
+ border-radius: 10px;
156
+ margin-bottom: 20px;
157
+ }}
158
+ .header h1 {{
159
+ font-size: 28px;
160
+ margin-bottom: 10px;
161
+ }}
162
+ .header .timestamp {{
163
+ opacity: 0.8;
164
+ }}
165
+ .summary {{
166
+ display: grid;
167
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
168
+ gap: 20px;
169
+ margin-bottom: 20px;
170
+ }}
171
+ .summary-card {{
172
+ background: white;
173
+ padding: 20px;
174
+ border-radius: 10px;
175
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
176
+ text-align: center;
177
+ }}
178
+ .summary-card .value {{
179
+ font-size: 36px;
180
+ font-weight: bold;
181
+ margin-bottom: 5px;
182
+ }}
183
+ .summary-card .label {{
184
+ color: #666;
185
+ }}
186
+ .summary-card.total .value {{ color: #409eff; }}
187
+ .summary-card.passed .value {{ color: #67c23a; }}
188
+ .summary-card.failed .value {{ color: #f56c6c; }}
189
+ .summary-card.rate .value {{ color: #e6a23c; }}
190
+ .results {{
191
+ background: white;
192
+ border-radius: 10px;
193
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
194
+ overflow: hidden;
195
+ }}
196
+ .results h2 {{
197
+ padding: 20px;
198
+ border-bottom: 1px solid #eee;
199
+ }}
200
+ table {{
201
+ width: 100%;
202
+ border-collapse: collapse;
203
+ }}
204
+ th, td {{
205
+ padding: 12px 15px;
206
+ text-align: left;
207
+ border-bottom: 1px solid #eee;
208
+ }}
209
+ th {{
210
+ background: #f8f9fa;
211
+ font-weight: 600;
212
+ }}
213
+ .case-row {{
214
+ cursor: pointer;
215
+ transition: background-color 0.2s;
216
+ }}
217
+ .case-row:hover {{
218
+ background-color: #f5f7fa;
219
+ }}
220
+ .status-passed {{
221
+ color: #67c23a;
222
+ font-weight: 600;
223
+ }}
224
+ .status-failed {{
225
+ color: #f56c6c;
226
+ font-weight: 600;
227
+ }}
228
+ .steps-table {{
229
+ margin: 10px;
230
+ background: #fafafa;
231
+ }}
232
+ .steps-table th {{
233
+ background: #f0f0f0;
234
+ }}
235
+ .error-text {{
236
+ color: #f56c6c;
237
+ font-size: 12px;
238
+ max-width: 300px;
239
+ overflow: hidden;
240
+ text-overflow: ellipsis;
241
+ white-space: nowrap;
242
+ }}
243
+ .footer {{
244
+ text-align: center;
245
+ padding: 20px;
246
+ color: #999;
247
+ }}
248
+ </style>
249
+ </head>
250
+ <body>
251
+ <div class="container">
252
+ <div class="header">
253
+ <h1>{title}</h1>
254
+ <div class="timestamp">生成时间: {timestamp}</div>
255
+ </div>
256
+
257
+ <div class="summary">
258
+ <div class="summary-card total">
259
+ <div class="value">{total}</div>
260
+ <div class="label">总用例数</div>
261
+ </div>
262
+ <div class="summary-card passed">
263
+ <div class="value">{passed}</div>
264
+ <div class="label">通过</div>
265
+ </div>
266
+ <div class="summary-card failed">
267
+ <div class="value">{failed}</div>
268
+ <div class="label">失败</div>
269
+ </div>
270
+ <div class="summary-card rate">
271
+ <div class="value">{pass_rate:.1f}%</div>
272
+ <div class="label">通过率</div>
273
+ </div>
274
+ <div class="summary-card">
275
+ <div class="value">{duration:.1f}s</div>
276
+ <div class="label">总耗时</div>
277
+ </div>
278
+ </div>
279
+
280
+ <div class="results">
281
+ <h2>测试用例详情</h2>
282
+ <table>
283
+ <thead>
284
+ <tr>
285
+ <th>#</th>
286
+ <th>用例ID</th>
287
+ <th>用例名称</th>
288
+ <th>Sheet</th>
289
+ <th>状态</th>
290
+ <th>耗时</th>
291
+ </tr>
292
+ </thead>
293
+ <tbody>
294
+ {cases_html}
295
+ </tbody>
296
+ </table>
297
+ </div>
298
+
299
+ <div class="footer">
300
+ Powered by KDTest-Playwright v2.0.0
301
+ </div>
302
+ </div>
303
+
304
+ <script>
305
+ function toggleSteps(row) {{
306
+ const stepsRow = row.nextElementSibling;
307
+ if (stepsRow.style.display === 'none') {{
308
+ stepsRow.style.display = 'table-row';
309
+ }} else {{
310
+ stepsRow.style.display = 'none';
311
+ }}
312
+ }}
313
+ </script>
314
+ </body>
315
+ </html>'''
316
+
317
+ def _generate_steps_html(self, step_results: List[StepResult]) -> str:
318
+ """生成步骤 HTML"""
319
+ rows = []
320
+ for i, step_result in enumerate(step_results, 1):
321
+ status_class = 'passed' if step_result.is_passed else 'failed'
322
+ status_text = '通过' if step_result.is_passed else '失败'
323
+ error_text = step_result.error or ''
324
+
325
+ rows.append(f'''
326
+ <tr>
327
+ <td>{i}</td>
328
+ <td>{step_result.step.keyword}</td>
329
+ <td>{step_result.step.description or '-'}</td>
330
+ <td class="status-{status_class}">{status_text}</td>
331
+ <td>{step_result.duration:.2f}s</td>
332
+ <td class="error-text" title="{error_text}">{error_text[:50]}</td>
333
+ </tr>
334
+ ''')
335
+
336
+ return '\n'.join(rows)
@@ -0,0 +1,123 @@
1
+ """日志管理器"""
2
+
3
+ import logging
4
+ import sys
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+
10
+ class Logger:
11
+ """日志管理器
12
+
13
+ 提供统一的日志记录功能。
14
+ """
15
+
16
+ _instance: Optional['Logger'] = None
17
+ _initialized: bool = False
18
+
19
+ def __new__(cls, *args, **kwargs) -> 'Logger':
20
+ if cls._instance is None:
21
+ cls._instance = super().__new__(cls)
22
+ return cls._instance
23
+
24
+ def __init__(
25
+ self,
26
+ name: str = 'kdtest',
27
+ level: int = logging.INFO,
28
+ log_file: Optional[str] = None,
29
+ console: bool = True
30
+ ):
31
+ if Logger._initialized:
32
+ return
33
+
34
+ self._logger = logging.getLogger(name)
35
+ self._logger.setLevel(level)
36
+ self._logger.handlers.clear()
37
+
38
+ # 日志格式
39
+ formatter = logging.Formatter(
40
+ '%(asctime)s | %(levelname)-8s | %(message)s',
41
+ datefmt='%Y-%m-%d %H:%M:%S'
42
+ )
43
+
44
+ # 控制台处理器
45
+ if console:
46
+ console_handler = logging.StreamHandler(sys.stdout)
47
+ console_handler.setLevel(level)
48
+ console_handler.setFormatter(formatter)
49
+ self._logger.addHandler(console_handler)
50
+
51
+ # 文件处理器
52
+ if log_file:
53
+ log_path = Path(log_file)
54
+ log_path.parent.mkdir(parents=True, exist_ok=True)
55
+
56
+ file_handler = logging.FileHandler(
57
+ str(log_path),
58
+ encoding='utf-8'
59
+ )
60
+ file_handler.setLevel(level)
61
+ file_handler.setFormatter(formatter)
62
+ self._logger.addHandler(file_handler)
63
+
64
+ Logger._initialized = True
65
+
66
+ def debug(self, message: str) -> None:
67
+ """调试日志"""
68
+ self._logger.debug(message)
69
+
70
+ def info(self, message: str) -> None:
71
+ """信息日志"""
72
+ self._logger.info(message)
73
+
74
+ def warning(self, message: str) -> None:
75
+ """警告日志"""
76
+ self._logger.warning(message)
77
+
78
+ def error(self, message: str) -> None:
79
+ """错误日志"""
80
+ self._logger.error(message)
81
+
82
+ def critical(self, message: str) -> None:
83
+ """严重错误日志"""
84
+ self._logger.critical(message)
85
+
86
+ def exception(self, message: str) -> None:
87
+ """异常日志(包含堆栈)"""
88
+ self._logger.exception(message)
89
+
90
+ @property
91
+ def logger(self) -> logging.Logger:
92
+ """获取底层 Logger"""
93
+ return self._logger
94
+
95
+ @classmethod
96
+ def get_instance(cls) -> 'Logger':
97
+ """获取单例实例"""
98
+ if cls._instance is None:
99
+ cls._instance = cls()
100
+ return cls._instance
101
+
102
+ @classmethod
103
+ def setup(
104
+ cls,
105
+ name: str = 'kdtest',
106
+ level: int = logging.INFO,
107
+ log_dir: str = 'result/log'
108
+ ) -> 'Logger':
109
+ """初始化日志系统
110
+
111
+ Args:
112
+ name: 日志名称
113
+ level: 日志级别
114
+ log_dir: 日志目录
115
+
116
+ Returns:
117
+ Logger: 日志实例
118
+ """
119
+ # 生成日志文件名
120
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
121
+ log_file = f"{log_dir}/{name}_{timestamp}.log"
122
+
123
+ return cls(name=name, level=level, log_file=log_file)