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,223 @@
|
|
|
1
|
+
"""Excel 测试用例解析器"""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ...reference import INFO
|
|
7
|
+
from ...common import EXCEL_COLUMNS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExcelReader:
|
|
11
|
+
"""Excel 测试用例读取器
|
|
12
|
+
|
|
13
|
+
兼容原 kdtest Excel 格式,解析测试用例文件。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, file_path: str):
|
|
17
|
+
"""初始化 Excel 读取器
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
file_path: Excel 文件路径
|
|
21
|
+
"""
|
|
22
|
+
self._file_path = Path(file_path)
|
|
23
|
+
self._workbook = None
|
|
24
|
+
self._sheets: List[str] = []
|
|
25
|
+
|
|
26
|
+
if not self._file_path.exists():
|
|
27
|
+
raise FileNotFoundError(f"Excel 文件不存在: {file_path}")
|
|
28
|
+
|
|
29
|
+
self._load_workbook()
|
|
30
|
+
|
|
31
|
+
def _load_workbook(self) -> None:
|
|
32
|
+
"""加载工作簿"""
|
|
33
|
+
try:
|
|
34
|
+
import openpyxl
|
|
35
|
+
self._workbook = openpyxl.load_workbook(str(self._file_path), data_only=True)
|
|
36
|
+
self._sheets = self._workbook.sheetnames
|
|
37
|
+
INFO(f"加载 Excel: {self._file_path.name}, Sheets: {self._sheets}")
|
|
38
|
+
except ImportError:
|
|
39
|
+
raise ImportError("请安装 openpyxl: pip install openpyxl")
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def sheet_names(self) -> List[str]:
|
|
43
|
+
"""获取所有 Sheet 名称"""
|
|
44
|
+
return self._sheets.copy()
|
|
45
|
+
|
|
46
|
+
def read_sheet(self, sheet_name: str) -> List[Dict[str, Any]]:
|
|
47
|
+
"""读取指定 Sheet 的测试用例
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
sheet_name: Sheet 名称
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List[Dict]: 测试用例列表
|
|
54
|
+
"""
|
|
55
|
+
if sheet_name not in self._sheets:
|
|
56
|
+
raise ValueError(f"Sheet 不存在: {sheet_name}")
|
|
57
|
+
|
|
58
|
+
sheet = self._workbook[sheet_name]
|
|
59
|
+
cases = []
|
|
60
|
+
current_case = None
|
|
61
|
+
steps = []
|
|
62
|
+
|
|
63
|
+
for row_idx, row in enumerate(sheet.iter_rows(min_row=1, values_only=True), start=1):
|
|
64
|
+
# 跳过空行
|
|
65
|
+
if not any(row):
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
# 获取各列值
|
|
69
|
+
case_id = self._get_cell_value(row, 0)
|
|
70
|
+
case_name = self._get_cell_value(row, 1)
|
|
71
|
+
description = self._get_cell_value(row, 2)
|
|
72
|
+
keyword = self._get_cell_value(row, 3)
|
|
73
|
+
|
|
74
|
+
# 获取定位信息 (E-N 列,索引 4-13)
|
|
75
|
+
locator_info = []
|
|
76
|
+
for i in range(4, 14):
|
|
77
|
+
val = self._get_cell_value(row, i)
|
|
78
|
+
if val:
|
|
79
|
+
locator_info.append(val)
|
|
80
|
+
|
|
81
|
+
# 获取操作值 (O 列,索引 14)
|
|
82
|
+
value = self._get_cell_value(row, 14)
|
|
83
|
+
|
|
84
|
+
# 判断是否是用例开始行
|
|
85
|
+
if case_id or case_name:
|
|
86
|
+
# 保存上一个用例
|
|
87
|
+
if current_case and steps:
|
|
88
|
+
current_case['steps'] = steps
|
|
89
|
+
cases.append(current_case)
|
|
90
|
+
|
|
91
|
+
# 开始新用例
|
|
92
|
+
current_case = {
|
|
93
|
+
'id': case_id or f"case_{len(cases) + 1}",
|
|
94
|
+
'name': case_name or f"用例{len(cases) + 1}",
|
|
95
|
+
'sheet': sheet_name,
|
|
96
|
+
'row': row_idx,
|
|
97
|
+
'steps': []
|
|
98
|
+
}
|
|
99
|
+
steps = []
|
|
100
|
+
|
|
101
|
+
# 如果当前行有关键字,也作为第一个步骤
|
|
102
|
+
if keyword:
|
|
103
|
+
steps.append({
|
|
104
|
+
'row': row_idx,
|
|
105
|
+
'description': description,
|
|
106
|
+
'keyword': keyword,
|
|
107
|
+
'locator': locator_info,
|
|
108
|
+
'value': value
|
|
109
|
+
})
|
|
110
|
+
elif keyword:
|
|
111
|
+
# 步骤行
|
|
112
|
+
steps.append({
|
|
113
|
+
'row': row_idx,
|
|
114
|
+
'description': description,
|
|
115
|
+
'keyword': keyword,
|
|
116
|
+
'locator': locator_info,
|
|
117
|
+
'value': value
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
# 保存最后一个用例
|
|
121
|
+
if current_case and steps:
|
|
122
|
+
current_case['steps'] = steps
|
|
123
|
+
cases.append(current_case)
|
|
124
|
+
|
|
125
|
+
INFO(f"读取 {sheet_name}: {len(cases)} 个用例")
|
|
126
|
+
return cases
|
|
127
|
+
|
|
128
|
+
def read_all_sheets(self, include_sheets: List[str] = None) -> Dict[str, List[Dict[str, Any]]]:
|
|
129
|
+
"""读取所有或指定 Sheet 的测试用例
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
include_sheets: 要包含的 Sheet 列表,None 表示全部
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dict[sheet_name, cases]: Sheet 名称到用例列表的映射
|
|
136
|
+
"""
|
|
137
|
+
result = {}
|
|
138
|
+
sheets_to_read = include_sheets or self._sheets
|
|
139
|
+
|
|
140
|
+
for sheet_name in sheets_to_read:
|
|
141
|
+
if sheet_name in self._sheets:
|
|
142
|
+
result[sheet_name] = self.read_sheet(sheet_name)
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
def _get_cell_value(self, row: tuple, index: int) -> Optional[str]:
|
|
147
|
+
"""获取单元格值
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
row: 行数据
|
|
151
|
+
index: 列索引
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
单元格值(字符串)
|
|
155
|
+
"""
|
|
156
|
+
if index >= len(row):
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
value = row[index]
|
|
160
|
+
if value is None:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
return str(value).strip()
|
|
164
|
+
|
|
165
|
+
def get_case_count(self, sheet_name: str = None) -> int:
|
|
166
|
+
"""获取用例数量
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
sheet_name: Sheet 名称,None 表示全部
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
int: 用例数量
|
|
173
|
+
"""
|
|
174
|
+
if sheet_name:
|
|
175
|
+
return len(self.read_sheet(sheet_name))
|
|
176
|
+
|
|
177
|
+
total = 0
|
|
178
|
+
for sheet in self._sheets:
|
|
179
|
+
total += len(self.read_sheet(sheet))
|
|
180
|
+
return total
|
|
181
|
+
|
|
182
|
+
def close(self) -> None:
|
|
183
|
+
"""关闭工作簿"""
|
|
184
|
+
if self._workbook:
|
|
185
|
+
self._workbook.close()
|
|
186
|
+
self._workbook = None
|
|
187
|
+
|
|
188
|
+
def __enter__(self) -> 'ExcelReader':
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
192
|
+
self.close()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestCase:
|
|
196
|
+
"""测试用例数据类"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, data: Dict[str, Any]):
|
|
199
|
+
self.id = data.get('id', '')
|
|
200
|
+
self.name = data.get('name', '')
|
|
201
|
+
self.sheet = data.get('sheet', '')
|
|
202
|
+
self.row = data.get('row', 0)
|
|
203
|
+
self.steps: List['TestStep'] = []
|
|
204
|
+
|
|
205
|
+
for step_data in data.get('steps', []):
|
|
206
|
+
self.steps.append(TestStep(step_data))
|
|
207
|
+
|
|
208
|
+
def __repr__(self) -> str:
|
|
209
|
+
return f"TestCase(id={self.id}, name={self.name}, steps={len(self.steps)})"
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestStep:
|
|
213
|
+
"""测试步骤数据类"""
|
|
214
|
+
|
|
215
|
+
def __init__(self, data: Dict[str, Any]):
|
|
216
|
+
self.row = data.get('row', 0)
|
|
217
|
+
self.description = data.get('description', '')
|
|
218
|
+
self.keyword = data.get('keyword', '')
|
|
219
|
+
self.locator = data.get('locator', [])
|
|
220
|
+
self.value = data.get('value')
|
|
221
|
+
|
|
222
|
+
def __repr__(self) -> str:
|
|
223
|
+
return f"TestStep(keyword={self.keyword}, value={self.value})"
|
kdtest_pw/cli/run.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""CLI 入口 - 命令行运行测试"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
from ..product import __version__
|
|
9
|
+
from ..core.browser_manager import BrowserManager
|
|
10
|
+
from ..core.config_loader import ConfigLoader
|
|
11
|
+
from ..action.key_retrieval import KeyRetrieval
|
|
12
|
+
from ..cases.case_collector import CaseCollector
|
|
13
|
+
from ..cases.case_executor import CaseExecutor
|
|
14
|
+
from ..cases.read.cell_handler import CellHandler
|
|
15
|
+
from ..plugins.plugin_loader import PluginLoader
|
|
16
|
+
from ..reference import GSTORE, GSDSTORE, INFO
|
|
17
|
+
from ..utils.log.html_report import HtmlReporter
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KDTestRunner:
|
|
21
|
+
"""KDTest Playwright 测试运行器"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config_path: Optional[str] = None):
|
|
24
|
+
"""初始化运行器
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config_path: 配置文件路径
|
|
28
|
+
"""
|
|
29
|
+
self._config_loader: Optional[ConfigLoader] = None
|
|
30
|
+
self._browser_manager: Optional[BrowserManager] = None
|
|
31
|
+
self._key_retrieval: Optional[KeyRetrieval] = None
|
|
32
|
+
self._plugin_loader: Optional[PluginLoader] = None
|
|
33
|
+
self._case_collector: Optional[CaseCollector] = None
|
|
34
|
+
self._case_executor: Optional[CaseExecutor] = None
|
|
35
|
+
self._reporter: Optional[HtmlReporter] = None
|
|
36
|
+
|
|
37
|
+
if config_path:
|
|
38
|
+
self.load_config(config_path)
|
|
39
|
+
|
|
40
|
+
def load_config(self, config_path: str) -> None:
|
|
41
|
+
"""加载配置文件
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config_path: 配置文件路径
|
|
45
|
+
"""
|
|
46
|
+
self._config_loader = ConfigLoader(config_path)
|
|
47
|
+
INFO(f"配置已加载: {config_path}")
|
|
48
|
+
|
|
49
|
+
def setup(self) -> None:
|
|
50
|
+
"""初始化测试环境"""
|
|
51
|
+
if not self._config_loader:
|
|
52
|
+
raise RuntimeError("请先加载配置文件")
|
|
53
|
+
|
|
54
|
+
config = self._config_loader.config
|
|
55
|
+
|
|
56
|
+
# 初始化浏览器管理器
|
|
57
|
+
self._browser_manager = BrowserManager()
|
|
58
|
+
|
|
59
|
+
# 启动浏览器
|
|
60
|
+
page = self._browser_manager.launch(
|
|
61
|
+
browser_type=self._config_loader.browser,
|
|
62
|
+
headless=self._config_loader.headless,
|
|
63
|
+
slow_mo=self._config_loader.slow_mo,
|
|
64
|
+
trace=self._config_loader.trace,
|
|
65
|
+
video=self._config_loader.video,
|
|
66
|
+
timeout=self._config_loader.timeout,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# 初始化关键字分发器
|
|
70
|
+
self._key_retrieval = KeyRetrieval(page)
|
|
71
|
+
|
|
72
|
+
# 初始化插件加载器
|
|
73
|
+
self._plugin_loader = PluginLoader(page)
|
|
74
|
+
self._plugin_loader.load_builtin_plugins()
|
|
75
|
+
|
|
76
|
+
# 合并插件关键字和元素数据
|
|
77
|
+
self._key_retrieval.register_plugin_keywords(
|
|
78
|
+
self._plugin_loader.get_all_keywords()
|
|
79
|
+
)
|
|
80
|
+
self._key_retrieval.set_element_data(
|
|
81
|
+
self._plugin_loader.get_all_element_data()
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# 初始化单元格处理器
|
|
85
|
+
cell_handler = CellHandler(
|
|
86
|
+
self._config_loader.self_defined_parameters
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# 初始化用例执行器
|
|
90
|
+
self._case_executor = CaseExecutor(
|
|
91
|
+
key_retrieval=self._key_retrieval,
|
|
92
|
+
cell_handler=cell_handler,
|
|
93
|
+
retry_count=self._config_loader.retry_count if self._config_loader.retry_switch else 0,
|
|
94
|
+
screenshot_on_fail=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# 初始化用例收集器
|
|
98
|
+
self._case_collector = CaseCollector(config)
|
|
99
|
+
|
|
100
|
+
# 初始化报告生成器
|
|
101
|
+
self._reporter = HtmlReporter()
|
|
102
|
+
|
|
103
|
+
# 导航到起始 URL
|
|
104
|
+
if self._config_loader.url:
|
|
105
|
+
page.goto(self._config_loader.url)
|
|
106
|
+
|
|
107
|
+
INFO("测试环境初始化完成")
|
|
108
|
+
|
|
109
|
+
def run(
|
|
110
|
+
self,
|
|
111
|
+
include_sheets: List[str] = None,
|
|
112
|
+
exclude_sheets: List[str] = None,
|
|
113
|
+
case_ids: List[str] = None,
|
|
114
|
+
case_pattern: str = None,
|
|
115
|
+
) -> dict:
|
|
116
|
+
"""运行测试
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
include_sheets: 只执行这些 Sheet
|
|
120
|
+
exclude_sheets: 排除这些 Sheet
|
|
121
|
+
case_ids: 只执行这些用例 ID
|
|
122
|
+
case_pattern: 用例名称匹配模式
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
dict: 执行摘要
|
|
126
|
+
"""
|
|
127
|
+
# 收集用例
|
|
128
|
+
all_cases = self._case_collector.collect()
|
|
129
|
+
|
|
130
|
+
# 过滤用例
|
|
131
|
+
cases = self._case_collector.filter_cases(
|
|
132
|
+
include_sheets=include_sheets,
|
|
133
|
+
exclude_sheets=exclude_sheets,
|
|
134
|
+
include_ids=case_ids,
|
|
135
|
+
name_pattern=case_pattern,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
INFO(f"过滤后用例数: {len(cases)}")
|
|
139
|
+
|
|
140
|
+
# 执行用例
|
|
141
|
+
results = self._case_executor.execute_cases(cases)
|
|
142
|
+
|
|
143
|
+
# 生成报告
|
|
144
|
+
if self._reporter:
|
|
145
|
+
report_path = self._reporter.generate(results)
|
|
146
|
+
INFO(f"测试报告: {report_path}")
|
|
147
|
+
|
|
148
|
+
return self._case_executor.summary
|
|
149
|
+
|
|
150
|
+
def teardown(self, save_trace: str = None) -> None:
|
|
151
|
+
"""清理测试环境
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
save_trace: trace 文件保存路径
|
|
155
|
+
"""
|
|
156
|
+
# 卸载插件
|
|
157
|
+
if self._plugin_loader:
|
|
158
|
+
self._plugin_loader.unload_all()
|
|
159
|
+
|
|
160
|
+
# 关闭浏览器
|
|
161
|
+
if self._browser_manager:
|
|
162
|
+
trace_path = save_trace or (
|
|
163
|
+
'result/trace/trace.zip' if self._config_loader and self._config_loader.trace else None
|
|
164
|
+
)
|
|
165
|
+
self._browser_manager.close(save_trace=trace_path)
|
|
166
|
+
|
|
167
|
+
INFO("测试环境已清理")
|
|
168
|
+
|
|
169
|
+
def __enter__(self) -> 'KDTestRunner':
|
|
170
|
+
self.setup()
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
174
|
+
self.teardown()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def parse_args() -> argparse.Namespace:
|
|
178
|
+
"""解析命令行参数"""
|
|
179
|
+
parser = argparse.ArgumentParser(
|
|
180
|
+
prog='kdtest-pw',
|
|
181
|
+
description='KDTest Playwright - 关键字驱动测试框架',
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
'-v', '--version',
|
|
186
|
+
action='version',
|
|
187
|
+
version=f'kdtest-pw {__version__}'
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
'-c', '--config',
|
|
192
|
+
type=str,
|
|
193
|
+
default='data/parameters.json',
|
|
194
|
+
help='配置文件路径 (默认: data/parameters.json)'
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
parser.add_argument(
|
|
198
|
+
'-s', '--sheets',
|
|
199
|
+
type=str,
|
|
200
|
+
nargs='+',
|
|
201
|
+
help='只执行指定的 Sheet'
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
'-x', '--exclude',
|
|
206
|
+
type=str,
|
|
207
|
+
nargs='+',
|
|
208
|
+
help='排除指定的 Sheet'
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
parser.add_argument(
|
|
212
|
+
'-i', '--ids',
|
|
213
|
+
type=str,
|
|
214
|
+
nargs='+',
|
|
215
|
+
help='只执行指定的用例 ID'
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
'-p', '--pattern',
|
|
220
|
+
type=str,
|
|
221
|
+
help='用例名称匹配模式 (支持通配符)'
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
'--browser',
|
|
226
|
+
type=str,
|
|
227
|
+
choices=['chromium', 'firefox', 'webkit'],
|
|
228
|
+
help='覆盖配置中的浏览器类型'
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
parser.add_argument(
|
|
232
|
+
'--headless',
|
|
233
|
+
action='store_true',
|
|
234
|
+
help='无头模式运行'
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
parser.add_argument(
|
|
238
|
+
'--trace',
|
|
239
|
+
action='store_true',
|
|
240
|
+
help='开启 trace 录制'
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
parser.add_argument(
|
|
244
|
+
'--video',
|
|
245
|
+
action='store_true',
|
|
246
|
+
help='开启视频录制'
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
'--slow-mo',
|
|
251
|
+
type=int,
|
|
252
|
+
help='操作延迟 (毫秒)'
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return parser.parse_args()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main() -> int:
|
|
259
|
+
"""主入口函数"""
|
|
260
|
+
args = parse_args()
|
|
261
|
+
|
|
262
|
+
# 检查配置文件
|
|
263
|
+
config_path = Path(args.config)
|
|
264
|
+
if not config_path.exists():
|
|
265
|
+
print(f"错误: 配置文件不存在: {config_path}")
|
|
266
|
+
return 1
|
|
267
|
+
|
|
268
|
+
runner = KDTestRunner(str(config_path))
|
|
269
|
+
|
|
270
|
+
# 覆盖配置
|
|
271
|
+
if args.browser:
|
|
272
|
+
runner._config_loader.set('browser', args.browser)
|
|
273
|
+
if args.headless:
|
|
274
|
+
runner._config_loader.set('headless', True)
|
|
275
|
+
if args.trace:
|
|
276
|
+
runner._config_loader.set('trace', True)
|
|
277
|
+
if args.video:
|
|
278
|
+
runner._config_loader.set('video', True)
|
|
279
|
+
if args.slow_mo:
|
|
280
|
+
runner._config_loader.set('slowMo', args.slow_mo)
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
runner.setup()
|
|
284
|
+
summary = runner.run(
|
|
285
|
+
include_sheets=args.sheets,
|
|
286
|
+
exclude_sheets=args.exclude,
|
|
287
|
+
case_ids=args.ids,
|
|
288
|
+
case_pattern=args.pattern,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# 输出摘要
|
|
292
|
+
print("\n" + "=" * 50)
|
|
293
|
+
print("测试执行摘要")
|
|
294
|
+
print("=" * 50)
|
|
295
|
+
print(f"总用例数: {summary['total']}")
|
|
296
|
+
print(f"通过: {summary['passed']}")
|
|
297
|
+
print(f"失败: {summary['failed']}")
|
|
298
|
+
print(f"跳过: {summary['skipped']}")
|
|
299
|
+
print(f"通过率: {summary['pass_rate']:.1f}%")
|
|
300
|
+
print(f"执行时间: {summary['duration']:.2f}s")
|
|
301
|
+
print("=" * 50)
|
|
302
|
+
|
|
303
|
+
return 0 if summary['failed'] == 0 else 1
|
|
304
|
+
|
|
305
|
+
except KeyboardInterrupt:
|
|
306
|
+
print("\n测试被中断")
|
|
307
|
+
return 130
|
|
308
|
+
except Exception as e:
|
|
309
|
+
print(f"执行错误: {e}")
|
|
310
|
+
import traceback
|
|
311
|
+
traceback.print_exc()
|
|
312
|
+
return 1
|
|
313
|
+
finally:
|
|
314
|
+
runner.teardown()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == '__main__':
|
|
318
|
+
sys.exit(main())
|
kdtest_pw/common.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""常量和数据存储模块"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, List
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# 项目根目录
|
|
7
|
+
PROJECT_ROOT = Path(__file__).parent
|
|
8
|
+
|
|
9
|
+
# 默认配置
|
|
10
|
+
DEFAULT_CONFIG: Dict[str, Any] = {
|
|
11
|
+
'browser': 'chromium',
|
|
12
|
+
'headless': False,
|
|
13
|
+
'slowMo': 0,
|
|
14
|
+
'timeout': 30000,
|
|
15
|
+
'trace': False,
|
|
16
|
+
'video': False,
|
|
17
|
+
'retrySwitch': False,
|
|
18
|
+
'retryCount': 1,
|
|
19
|
+
'parallel': False,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# 支持的浏览器类型
|
|
23
|
+
BROWSER_TYPES: List[str] = ['chromium', 'firefox', 'webkit']
|
|
24
|
+
|
|
25
|
+
# 定位器类型映射
|
|
26
|
+
LOCATOR_TYPES: Dict[str, str] = {
|
|
27
|
+
'id': 'id',
|
|
28
|
+
'name': 'name',
|
|
29
|
+
'class': 'class_name',
|
|
30
|
+
'class_name': 'class_name',
|
|
31
|
+
'css': 'css',
|
|
32
|
+
'xpath': 'xpath',
|
|
33
|
+
'text': 'text',
|
|
34
|
+
'role': 'role',
|
|
35
|
+
'data_testid': 'data_testid',
|
|
36
|
+
'placeholder': 'placeholder',
|
|
37
|
+
'link_text': 'text',
|
|
38
|
+
'partial_link_text': 'text',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 键盘键映射 (Selenium Keys -> Playwright)
|
|
42
|
+
KEY_MAP: Dict[str, str] = {
|
|
43
|
+
'Keys.ENTER': 'Enter',
|
|
44
|
+
'Keys.TAB': 'Tab',
|
|
45
|
+
'Keys.BACK_SPACE': 'Backspace',
|
|
46
|
+
'Keys.BACKSPACE': 'Backspace',
|
|
47
|
+
'Keys.ESCAPE': 'Escape',
|
|
48
|
+
'Keys.DELETE': 'Delete',
|
|
49
|
+
'Keys.ARROW_UP': 'ArrowUp',
|
|
50
|
+
'Keys.ARROW_DOWN': 'ArrowDown',
|
|
51
|
+
'Keys.ARROW_LEFT': 'ArrowLeft',
|
|
52
|
+
'Keys.ARROW_RIGHT': 'ArrowRight',
|
|
53
|
+
'Keys.HOME': 'Home',
|
|
54
|
+
'Keys.END': 'End',
|
|
55
|
+
'Keys.PAGE_UP': 'PageUp',
|
|
56
|
+
'Keys.PAGE_DOWN': 'PageDown',
|
|
57
|
+
'Keys.SPACE': 'Space',
|
|
58
|
+
'Keys.CONTROL': 'Control',
|
|
59
|
+
'Keys.ALT': 'Alt',
|
|
60
|
+
'Keys.SHIFT': 'Shift',
|
|
61
|
+
'Keys.F1': 'F1',
|
|
62
|
+
'Keys.F2': 'F2',
|
|
63
|
+
'Keys.F3': 'F3',
|
|
64
|
+
'Keys.F4': 'F4',
|
|
65
|
+
'Keys.F5': 'F5',
|
|
66
|
+
'Keys.F6': 'F6',
|
|
67
|
+
'Keys.F7': 'F7',
|
|
68
|
+
'Keys.F8': 'F8',
|
|
69
|
+
'Keys.F9': 'F9',
|
|
70
|
+
'Keys.F10': 'F10',
|
|
71
|
+
'Keys.F11': 'F11',
|
|
72
|
+
'Keys.F12': 'F12',
|
|
73
|
+
# 组合键
|
|
74
|
+
'Keys.CONTROL+a': 'Control+a',
|
|
75
|
+
'Keys.CONTROL+c': 'Control+c',
|
|
76
|
+
'Keys.CONTROL+v': 'Control+v',
|
|
77
|
+
'Keys.CONTROL+x': 'Control+x',
|
|
78
|
+
'Keys.CONTROL+z': 'Control+z',
|
|
79
|
+
'Keys.CONTROL+s': 'Control+s',
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# 断言模式
|
|
83
|
+
ASSERT_MODES: List[str] = ['equals', 'contains', 'regex', 'gt', 'lt', 'gte', 'lte']
|
|
84
|
+
|
|
85
|
+
# 元素等待状态
|
|
86
|
+
WAIT_STATES: List[str] = ['visible', 'hidden', 'attached', 'detached']
|
|
87
|
+
|
|
88
|
+
# 结果目录
|
|
89
|
+
RESULT_DIRS: Dict[str, str] = {
|
|
90
|
+
'report': 'result/report',
|
|
91
|
+
'log': 'result/log',
|
|
92
|
+
'trace': 'result/trace',
|
|
93
|
+
'video': 'result/video',
|
|
94
|
+
'screenshot': 'result/screenshot',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Excel列映射
|
|
98
|
+
EXCEL_COLUMNS: Dict[str, int] = {
|
|
99
|
+
'case_id': 0, # A列 - 用例ID
|
|
100
|
+
'case_name': 1, # B列 - 用例名称
|
|
101
|
+
'description': 2, # C列 - 步骤描述
|
|
102
|
+
'keyword': 3, # D列 - 关键字
|
|
103
|
+
'locator_start': 4, # E列开始 - 定位信息
|
|
104
|
+
'locator_end': 13, # N列结束 - 定位信息
|
|
105
|
+
'value': 14, # O列 - 操作值
|
|
106
|
+
}
|