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
kdtest_pw/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """KDTest-Playwright 关键字驱动测试框架
2
+
3
+ 基于 Playwright 的关键字驱动测试框架,专为 Element Plus/Vue3 优化。
4
+ """
5
+
6
+ from .product import __version__, __author__, __description__
7
+ from .reference import (
8
+ GSTORE,
9
+ GSDSTORE,
10
+ MODULEDATA,
11
+ PRIVATEDATA,
12
+ CASESDATA,
13
+ INFO,
14
+ get_global,
15
+ set_global,
16
+ get_element_value,
17
+ set_element_value,
18
+ clear_element_values,
19
+ )
20
+ from .common import (
21
+ DEFAULT_CONFIG,
22
+ BROWSER_TYPES,
23
+ LOCATOR_TYPES,
24
+ KEY_MAP,
25
+ )
26
+
27
+ __all__ = [
28
+ # 版本信息
29
+ '__version__',
30
+ '__author__',
31
+ '__description__',
32
+ # 全局存储
33
+ 'GSTORE',
34
+ 'GSDSTORE',
35
+ 'MODULEDATA',
36
+ 'PRIVATEDATA',
37
+ 'CASESDATA',
38
+ # 工具函数
39
+ 'INFO',
40
+ 'get_global',
41
+ 'set_global',
42
+ 'get_element_value',
43
+ 'set_element_value',
44
+ 'clear_element_values',
45
+ # 常量
46
+ 'DEFAULT_CONFIG',
47
+ 'BROWSER_TYPES',
48
+ 'LOCATOR_TYPES',
49
+ 'KEY_MAP',
50
+ ]
@@ -0,0 +1,7 @@
1
+ """动作模块 - 关键字实现"""
2
+
3
+ from .base_keyword import BaseKeyword
4
+ from .page_action import KeyWordTest
5
+ from .key_retrieval import KeyRetrieval
6
+
7
+ __all__ = ['BaseKeyword', 'KeyWordTest', 'KeyRetrieval']
@@ -0,0 +1,292 @@
1
+ """基础关键字类 - 元素定位和通用操作"""
2
+
3
+ from playwright.sync_api import Page, Locator, FrameLocator
4
+ from typing import Optional, Union, Literal, List
5
+
6
+ from ..common import LOCATOR_TYPES
7
+ from ..reference import INFO
8
+
9
+ LocatorType = Literal[
10
+ 'id', 'name', 'class_name', 'class', 'css', 'xpath',
11
+ 'text', 'role', 'data_testid', 'placeholder', 'link_text', 'partial_link_text'
12
+ ]
13
+
14
+
15
+ class BaseKeyword:
16
+ """基础关键字类
17
+
18
+ 提供元素定位和通用操作方法,是所有关键字类的基类。
19
+ """
20
+
21
+ def __init__(self, page: Page):
22
+ """初始化基础关键字
23
+
24
+ Args:
25
+ page: Playwright Page 实例
26
+ """
27
+ self.page = page
28
+ self._current_frame: Optional[FrameLocator] = None
29
+ self._frame_stack: List[FrameLocator] = []
30
+
31
+ def locator(
32
+ self,
33
+ targeting: LocatorType,
34
+ element: str,
35
+ index: Optional[Union[int, str]] = None,
36
+ parent: Optional[Locator] = None
37
+ ) -> Locator:
38
+ """统一元素定位方法
39
+
40
+ Args:
41
+ targeting: 定位类型 (id, name, class_name, css, xpath, text, role, data_testid, placeholder)
42
+ element: 定位表达式
43
+ index: 索引 (数字、'first'、'last'),None 表示不使用索引
44
+ parent: 父元素定位器,用于链式定位
45
+
46
+ Returns:
47
+ Locator: Playwright Locator 实例
48
+ """
49
+ # 确定基础定位上下文
50
+ base = parent or self._current_frame or self.page
51
+
52
+ # 标准化定位类型
53
+ targeting = LOCATOR_TYPES.get(targeting, targeting)
54
+
55
+ # 根据定位类型创建 Locator
56
+ locator_map = {
57
+ 'id': lambda: base.locator(f'#{element}'),
58
+ 'name': lambda: base.locator(f'[name="{element}"]'),
59
+ 'class_name': lambda: base.locator(f'.{element}'),
60
+ 'css': lambda: base.locator(element),
61
+ 'xpath': lambda: base.locator(f'xpath={element}'),
62
+ 'text': lambda: base.get_by_text(element, exact=False),
63
+ 'role': lambda: base.get_by_role(element),
64
+ 'data_testid': lambda: base.get_by_test_id(element),
65
+ 'placeholder': lambda: base.get_by_placeholder(element),
66
+ }
67
+
68
+ loc = locator_map.get(targeting, lambda: base.locator(element))()
69
+
70
+ # 处理索引
71
+ if index is not None:
72
+ if index == 'first' or index == 0:
73
+ loc = loc.first
74
+ elif index == 'last' or index == -1:
75
+ loc = loc.last
76
+ elif isinstance(index, int):
77
+ loc = loc.nth(index)
78
+ elif isinstance(index, str) and index.isdigit():
79
+ loc = loc.nth(int(index))
80
+
81
+ return loc
82
+
83
+ def locator_by_expression(self, expression: str, index: Optional[Union[int, str]] = None) -> Locator:
84
+ """通过表达式定位元素
85
+
86
+ 支持格式:
87
+ - "xpath=//div" 或 "xpath://div"
88
+ - "css=.class" 或 "css:.class"
89
+ - "text=文本"
90
+ - "id=elementId"
91
+ - 纯 CSS 选择器
92
+
93
+ Args:
94
+ expression: 定位表达式
95
+ index: 索引
96
+
97
+ Returns:
98
+ Locator: Playwright Locator 实例
99
+ """
100
+ base = self._current_frame or self.page
101
+
102
+ # 解析表达式
103
+ if '=' in expression:
104
+ parts = expression.split('=', 1)
105
+ prefix = parts[0].lower().rstrip(':')
106
+ value = parts[1]
107
+
108
+ if prefix == 'xpath':
109
+ loc = base.locator(f'xpath={value}')
110
+ elif prefix == 'css':
111
+ loc = base.locator(value)
112
+ elif prefix == 'text':
113
+ loc = base.get_by_text(value)
114
+ elif prefix == 'id':
115
+ loc = base.locator(f'#{value}')
116
+ elif prefix == 'name':
117
+ loc = base.locator(f'[name="{value}"]')
118
+ elif prefix == 'class':
119
+ loc = base.locator(f'.{value}')
120
+ else:
121
+ loc = base.locator(expression)
122
+ else:
123
+ # 纯 CSS 选择器
124
+ loc = base.locator(expression)
125
+
126
+ # 处理索引
127
+ if index is not None:
128
+ if index == 'first' or index == 0:
129
+ loc = loc.first
130
+ elif index == 'last' or index == -1:
131
+ loc = loc.last
132
+ elif isinstance(index, int):
133
+ loc = loc.nth(index)
134
+
135
+ return loc
136
+
137
+ def switch_frame(self, targeting: str, element: str) -> None:
138
+ """切换到 iframe
139
+
140
+ Args:
141
+ targeting: 定位类型
142
+ element: 定位表达式
143
+ """
144
+ if targeting == 'xpath':
145
+ frame_locator = self.page.frame_locator(f'xpath={element}')
146
+ elif targeting in ('css', 'selector'):
147
+ frame_locator = self.page.frame_locator(element)
148
+ elif targeting == 'name':
149
+ frame_locator = self.page.frame_locator(f'[name="{element}"]')
150
+ elif targeting == 'id':
151
+ frame_locator = self.page.frame_locator(f'#{element}')
152
+ else:
153
+ frame_locator = self.page.frame_locator(element)
154
+
155
+ self._frame_stack.append(self._current_frame)
156
+ self._current_frame = frame_locator
157
+ INFO(f"切换到 iframe: {element}")
158
+
159
+ def switch_frame_parent(self) -> None:
160
+ """切换到父级 frame"""
161
+ if self._frame_stack:
162
+ self._current_frame = self._frame_stack.pop()
163
+ else:
164
+ self._current_frame = None
165
+ INFO("切换到父级 frame")
166
+
167
+ def frame_default(self) -> None:
168
+ """返回主文档"""
169
+ self._current_frame = None
170
+ self._frame_stack.clear()
171
+ INFO("切换到主文档")
172
+
173
+ def wait_for_loading(self, timeout: int = 30000) -> None:
174
+ """等待页面加载完成
175
+
176
+ Args:
177
+ timeout: 超时时间 (毫秒)
178
+ """
179
+ self.page.wait_for_load_state('networkidle', timeout=timeout)
180
+
181
+ def wait_for_dom_ready(self, timeout: int = 30000) -> None:
182
+ """等待 DOM 加载完成
183
+
184
+ Args:
185
+ timeout: 超时时间 (毫秒)
186
+ """
187
+ self.page.wait_for_load_state('domcontentloaded', timeout=timeout)
188
+
189
+ def wait_for_element_plus_loading(self, timeout: int = 30000) -> None:
190
+ """等待 Element Plus 的 v-loading 消失
191
+
192
+ Args:
193
+ timeout: 超时时间 (毫秒)
194
+ """
195
+ loading = self.page.locator('.el-loading-mask')
196
+ try:
197
+ if loading.count() > 0:
198
+ loading.first.wait_for(state='hidden', timeout=timeout)
199
+ except Exception:
200
+ pass # loading 可能已经消失
201
+
202
+ def wait_for_element(
203
+ self,
204
+ targeting: str,
205
+ element: str,
206
+ state: str = 'visible',
207
+ timeout: int = 30000
208
+ ) -> None:
209
+ """等待元素状态
210
+
211
+ Args:
212
+ targeting: 定位类型
213
+ element: 定位表达式
214
+ state: 等待状态 (visible, hidden, attached, detached)
215
+ timeout: 超时时间 (毫秒)
216
+ """
217
+ loc = self.locator(targeting, element)
218
+ loc.wait_for(state=state, timeout=timeout)
219
+
220
+ def is_element_visible(self, targeting: str, element: str, index: Optional[int] = None) -> bool:
221
+ """检查元素是否可见
222
+
223
+ Args:
224
+ targeting: 定位类型
225
+ element: 定位表达式
226
+ index: 索引
227
+
228
+ Returns:
229
+ bool: 元素是否可见
230
+ """
231
+ loc = self.locator(targeting, element, index)
232
+ return loc.is_visible()
233
+
234
+ def is_element_enabled(self, targeting: str, element: str, index: Optional[int] = None) -> bool:
235
+ """检查元素是否可用
236
+
237
+ Args:
238
+ targeting: 定位类型
239
+ element: 定位表达式
240
+ index: 索引
241
+
242
+ Returns:
243
+ bool: 元素是否可用
244
+ """
245
+ loc = self.locator(targeting, element, index)
246
+ return loc.is_enabled()
247
+
248
+ def get_element_count(self, targeting: str, element: str) -> int:
249
+ """获取元素数量
250
+
251
+ Args:
252
+ targeting: 定位类型
253
+ element: 定位表达式
254
+
255
+ Returns:
256
+ int: 元素数量
257
+ """
258
+ loc = self.locator(targeting, element)
259
+ return loc.count()
260
+
261
+ def execute_script(self, script: str, *args) -> any:
262
+ """执行 JavaScript
263
+
264
+ Args:
265
+ script: JavaScript 代码
266
+ *args: 脚本参数
267
+
268
+ Returns:
269
+ 脚本执行结果
270
+ """
271
+ return self.page.evaluate(script, args if args else None)
272
+
273
+ def execute_script_on_element(
274
+ self,
275
+ targeting: str,
276
+ element: str,
277
+ script: str,
278
+ index: Optional[int] = None
279
+ ) -> any:
280
+ """在元素上执行 JavaScript
281
+
282
+ Args:
283
+ targeting: 定位类型
284
+ element: 定位表达式
285
+ script: JavaScript 代码 (使用 el 引用元素)
286
+ index: 索引
287
+
288
+ Returns:
289
+ 脚本执行结果
290
+ """
291
+ loc = self.locator(targeting, element, index)
292
+ return loc.evaluate(script)
@@ -0,0 +1,23 @@
1
+ """Element Plus 专用关键字模块"""
2
+
3
+ from .el_select import ElSelectKeyword
4
+ from .el_datepicker import ElDatePickerKeyword
5
+ from .el_dialog import ElDialogKeyword
6
+ from .el_table import ElTableKeyword
7
+ from .el_cascader import ElCascaderKeyword
8
+ from .el_tree import ElTreeKeyword
9
+ from .el_upload import ElUploadKeyword
10
+ from .el_form import ElFormKeyword
11
+ from .el_menu import ElMenuKeyword
12
+
13
+ __all__ = [
14
+ 'ElSelectKeyword',
15
+ 'ElDatePickerKeyword',
16
+ 'ElDialogKeyword',
17
+ 'ElTableKeyword',
18
+ 'ElCascaderKeyword',
19
+ 'ElTreeKeyword',
20
+ 'ElUploadKeyword',
21
+ 'ElFormKeyword',
22
+ 'ElMenuKeyword',
23
+ ]
@@ -0,0 +1,263 @@
1
+ """Element Plus Cascader 组件关键字"""
2
+
3
+ from playwright.sync_api import Page, Locator
4
+ from typing import Optional, List
5
+
6
+ from ..base_keyword import BaseKeyword
7
+ from ...reference import INFO, set_element_value
8
+
9
+
10
+ class ElCascaderKeyword(BaseKeyword):
11
+ """Element Plus Cascader 级联选择器关键字
12
+
13
+ 处理级联选择器的多级选择、搜索等操作。
14
+ """
15
+
16
+ def __init__(self, page: Page):
17
+ super().__init__(page)
18
+
19
+ def el_cascader(
20
+ self,
21
+ targeting: str,
22
+ element: str,
23
+ index: Optional[int] = None,
24
+ *,
25
+ content: str
26
+ ) -> None:
27
+ """级联选择
28
+
29
+ Args:
30
+ targeting: 定位类型
31
+ element: 定位表达式
32
+ index: 索引
33
+ content: 选项路径 (格式: "一级/二级/三级")
34
+ """
35
+ options = [opt.strip() for opt in str(content).split('/')]
36
+ INFO(f"级联选择: {options}")
37
+
38
+ cascader = self.locator(targeting, element, index)
39
+ cascader.click()
40
+
41
+ # 等待面板出现
42
+ panel = self.page.locator('.el-cascader-panel:visible, .el-cascader__dropdown:visible')
43
+ panel.wait_for(state='visible', timeout=5000)
44
+
45
+ # 逐级选择
46
+ for i, opt in enumerate(options):
47
+ # 找到当前级别的菜单
48
+ menus = panel.locator('.el-cascader-menu')
49
+ if menus.count() > i:
50
+ menu = menus.nth(i)
51
+ node = menu.locator(f'.el-cascader-node:has-text("{opt}")')
52
+ if node.count() > 0:
53
+ node.first.click()
54
+ self.page.wait_for_timeout(200)
55
+
56
+ # 关闭面板
57
+ self.page.keyboard.press('Escape')
58
+
59
+ def el_cascader_search(
60
+ self,
61
+ targeting: str,
62
+ element: str,
63
+ index: Optional[int] = None,
64
+ *,
65
+ content: str,
66
+ select_first: bool = True
67
+ ) -> None:
68
+ """搜索并选择
69
+
70
+ Args:
71
+ targeting: 定位类型
72
+ element: 定位表达式
73
+ index: 索引
74
+ content: 搜索关键词
75
+ select_first: 是否选择第一个结果
76
+ """
77
+ INFO(f"级联搜索: {content}")
78
+
79
+ cascader = self.locator(targeting, element, index)
80
+ cascader.click()
81
+
82
+ # 输入搜索
83
+ input_el = cascader.locator('.el-input__inner')
84
+ input_el.fill(str(content))
85
+ self.page.wait_for_timeout(300)
86
+
87
+ # 选择结果
88
+ if select_first:
89
+ suggestion = self.page.locator('.el-cascader__suggestion-panel:visible, .el-cascader-panel:visible')
90
+ if suggestion.is_visible():
91
+ first_item = suggestion.locator('.el-cascader__suggestion-item, .el-cascader-node').first
92
+ if first_item.is_visible():
93
+ first_item.click()
94
+
95
+ def el_cascader_clear(
96
+ self,
97
+ targeting: str,
98
+ element: str,
99
+ index: Optional[int] = None
100
+ ) -> None:
101
+ """清除选择
102
+
103
+ Args:
104
+ targeting: 定位类型
105
+ element: 定位表达式
106
+ index: 索引
107
+ """
108
+ INFO("清除级联选择")
109
+
110
+ cascader = self.locator(targeting, element, index)
111
+ cascader.hover()
112
+ self.page.wait_for_timeout(200)
113
+
114
+ clear_btn = cascader.locator('.el-cascader__clear-icon, .el-icon-circle-close')
115
+ if clear_btn.is_visible():
116
+ clear_btn.click()
117
+
118
+ def el_cascader_get_value(
119
+ self,
120
+ targeting: str,
121
+ element: str,
122
+ index: Optional[int] = None
123
+ ) -> str:
124
+ """获取当前选中值
125
+
126
+ Args:
127
+ targeting: 定位类型
128
+ element: 定位表达式
129
+ index: 索引
130
+
131
+ Returns:
132
+ str: 选中的值(路径格式)
133
+ """
134
+ cascader = self.locator(targeting, element, index)
135
+
136
+ # 尝试获取标签
137
+ tags = cascader.locator('.el-cascader__tags .el-tag')
138
+ if tags.count() > 0:
139
+ values = [tag.text_content().strip() for tag in tags.all()]
140
+ return ' / '.join(values)
141
+
142
+ # 获取输入框值
143
+ input_el = cascader.locator('.el-input__inner')
144
+ value = input_el.get_attribute('value') or input_el.text_content()
145
+ return (value or '').strip()
146
+
147
+ def el_cascader_expand(
148
+ self,
149
+ targeting: str,
150
+ element: str,
151
+ index: Optional[int] = None,
152
+ *,
153
+ content: str
154
+ ) -> None:
155
+ """展开到指定节点(不选择)
156
+
157
+ Args:
158
+ targeting: 定位类型
159
+ element: 定位表达式
160
+ index: 索引
161
+ content: 展开路径 (格式: "一级/二级")
162
+ """
163
+ options = [opt.strip() for opt in str(content).split('/')]
164
+ INFO(f"展开级联: {options}")
165
+
166
+ cascader = self.locator(targeting, element, index)
167
+ cascader.click()
168
+
169
+ panel = self.page.locator('.el-cascader-panel:visible')
170
+ panel.wait_for(state='visible', timeout=5000)
171
+
172
+ # 逐级展开(不选择叶子节点)
173
+ for i, opt in enumerate(options):
174
+ menus = panel.locator('.el-cascader-menu')
175
+ if menus.count() > i:
176
+ menu = menus.nth(i)
177
+ # 悬停展开
178
+ node = menu.locator(f'.el-cascader-node:has-text("{opt}")')
179
+ if node.count() > 0:
180
+ node.first.hover()
181
+ self.page.wait_for_timeout(200)
182
+
183
+ def el_cascader_select_multiple(
184
+ self,
185
+ targeting: str,
186
+ element: str,
187
+ index: Optional[int] = None,
188
+ *,
189
+ content: str
190
+ ) -> None:
191
+ """多选级联
192
+
193
+ Args:
194
+ targeting: 定位类型
195
+ element: 定位表达式
196
+ index: 索引
197
+ content: 多个路径 (格式: "一级/二级,一级/三级")
198
+ """
199
+ paths = [path.strip() for path in str(content).split(',')]
200
+ INFO(f"级联多选: {paths}")
201
+
202
+ cascader = self.locator(targeting, element, index)
203
+ cascader.click()
204
+
205
+ panel = self.page.locator('.el-cascader-panel:visible')
206
+ panel.wait_for(state='visible', timeout=5000)
207
+
208
+ for path in paths:
209
+ options = [opt.strip() for opt in path.split('/')]
210
+
211
+ # 逐级选择
212
+ for i, opt in enumerate(options):
213
+ menus = panel.locator('.el-cascader-menu')
214
+ if menus.count() > i:
215
+ menu = menus.nth(i)
216
+ node = menu.locator(f'.el-cascader-node:has-text("{opt}")')
217
+ if node.count() > 0:
218
+ # 如果是最后一级,点击复选框
219
+ if i == len(options) - 1:
220
+ checkbox = node.first.locator('.el-checkbox')
221
+ if checkbox.count() > 0:
222
+ checkbox.click()
223
+ else:
224
+ node.first.click()
225
+ else:
226
+ node.first.click()
227
+ self.page.wait_for_timeout(200)
228
+
229
+ # 关闭面板
230
+ self.page.keyboard.press('Escape')
231
+
232
+ def el_cascader_assert(
233
+ self,
234
+ targeting: str,
235
+ element: str,
236
+ index: Optional[int] = None,
237
+ *,
238
+ content: str,
239
+ mode: str = 'equals'
240
+ ) -> bool:
241
+ """断言选中值
242
+
243
+ Args:
244
+ targeting: 定位类型
245
+ element: 定位表达式
246
+ index: 索引
247
+ content: 期望值
248
+ mode: 断言模式
249
+
250
+ Returns:
251
+ bool: 断言结果
252
+ """
253
+ actual = self.el_cascader_get_value(targeting, element, index)
254
+
255
+ if mode == 'equals':
256
+ result = actual == content
257
+ elif mode == 'contains':
258
+ result = content in actual
259
+ else:
260
+ result = actual == content
261
+
262
+ INFO(f"级联断言: '{actual}' {mode} '{content}' -> {result}")
263
+ return result