devhelmkit 0.1.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.
- devhelmkit/__init__.py +44 -0
- devhelmkit/android/__init__.py +7 -0
- devhelmkit/android/driver.py +18 -0
- devhelmkit/assets/so/arm64-v8a/agent_v1.so +0 -0
- devhelmkit/assets/so/arm64-v8a/agent_v2.so +0 -0
- devhelmkit/assets/so/x86_64/agent.so +0 -0
- devhelmkit/core/__init__.py +3 -0
- devhelmkit/core/base_component.py +214 -0
- devhelmkit/core/base_driver.py +414 -0
- devhelmkit/core/base_window.py +25 -0
- devhelmkit/core/selector_spec.py +102 -0
- devhelmkit/entry.py +90 -0
- devhelmkit/exceptions.py +56 -0
- devhelmkit/harmony/__init__.py +3 -0
- devhelmkit/harmony/agent/__init__.py +7 -0
- devhelmkit/harmony/agent/so_manager.py +300 -0
- devhelmkit/harmony/config.py +124 -0
- devhelmkit/harmony/device/__init__.py +6 -0
- devhelmkit/harmony/device/hdc.py +430 -0
- devhelmkit/harmony/driver.py +1416 -0
- devhelmkit/harmony/finder/__init__.py +12 -0
- devhelmkit/harmony/finder/component_finder.py +336 -0
- devhelmkit/harmony/finder/popup_handler.py +81 -0
- devhelmkit/harmony/finder/selector_adapter.py +101 -0
- devhelmkit/harmony/finder/xpath_query.py +110 -0
- devhelmkit/harmony/rpc/__init__.py +12 -0
- devhelmkit/harmony/rpc/client.py +126 -0
- devhelmkit/harmony/rpc/proxy_v2.py +106 -0
- devhelmkit/harmony/rpc/remote_object.py +48 -0
- devhelmkit/harmony/uiobject.py +246 -0
- devhelmkit/harmony/uiwindow.py +43 -0
- devhelmkit/harmony/webview/__init__.py +17 -0
- devhelmkit/harmony/webview/chromedriver_manager.py +251 -0
- devhelmkit/harmony/webview/devtools_finder.py +131 -0
- devhelmkit/harmony/webview/webview_driver.py +326 -0
- devhelmkit/model/__init__.py +3 -0
- devhelmkit/model/action.py +147 -0
- devhelmkit/model/app_state.py +50 -0
- devhelmkit/model/constants.py +22 -0
- devhelmkit/model/display.py +32 -0
- devhelmkit/model/format_string.py +24 -0
- devhelmkit/model/input.py +50 -0
- devhelmkit/model/json_base.py +42 -0
- devhelmkit/model/keys.py +375 -0
- devhelmkit/model/match_pattern.py +15 -0
- devhelmkit/model/page.py +13 -0
- devhelmkit/model/params.py +42 -0
- devhelmkit/model/rect.py +58 -0
- devhelmkit/model/runnable.py +18 -0
- devhelmkit/utils/__init__.py +3 -0
- devhelmkit/utils/logger.py +72 -0
- devhelmkit/utils/retry.py +46 -0
- devhelmkit/utils/timeout.py +64 -0
- devhelmkit-0.1.0.dist-info/METADATA +411 -0
- devhelmkit-0.1.0.dist-info/RECORD +58 -0
- devhelmkit-0.1.0.dist-info/WHEEL +5 -0
- devhelmkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- devhelmkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""BaseDriver:跨平台设备驱动契约。
|
|
4
|
+
|
|
5
|
+
命名原则:U2 风格 snake_case 为基准,语义化方法为补充。
|
|
6
|
+
抽象层只定义跨平台通用接口,平台特有能力通过子类扩展提供。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from PIL.Image import Image
|
|
15
|
+
from devhelmkit.core.base_component import BaseComponent
|
|
16
|
+
from devhelmkit.core.base_window import BaseWindow
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseDriver(ABC):
|
|
20
|
+
"""跨平台设备驱动契约。"""
|
|
21
|
+
|
|
22
|
+
# ============================================================
|
|
23
|
+
# 生命周期
|
|
24
|
+
# ============================================================
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def close(self) -> None:
|
|
28
|
+
"""关闭驱动,释放连接资源。"""
|
|
29
|
+
|
|
30
|
+
def __enter__(self) -> 'BaseDriver':
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
34
|
+
self.close()
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def device_sn(self) -> str:
|
|
40
|
+
"""设备序列号。"""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def platform(self) -> str:
|
|
45
|
+
"""平台标识:'harmony' / 'android'。"""
|
|
46
|
+
|
|
47
|
+
# ============================================================
|
|
48
|
+
# 设备信息
|
|
49
|
+
# ============================================================
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
@abstractmethod
|
|
53
|
+
def info(self) -> Dict[str, Any]:
|
|
54
|
+
"""设备基本信息(品牌、型号、系统版本等)。"""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def get_display_size(self) -> Tuple[int, int]:
|
|
58
|
+
"""获取屏幕分辨率 (width, height)。"""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_display_rotation(self) -> int:
|
|
62
|
+
"""获取屏幕方向。"""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def set_display_rotation(self, rotation: int) -> None:
|
|
66
|
+
"""设置屏幕方向。"""
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def set_display_rotation_enabled(self, enabled: bool) -> None:
|
|
70
|
+
"""自动旋转开关。"""
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def get_device_type(self) -> str:
|
|
74
|
+
"""设备类型(phone / tablet / 2in1 / wearable / ...)。"""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def get_os_type(self) -> str:
|
|
78
|
+
"""操作系统类型标识。"""
|
|
79
|
+
|
|
80
|
+
# ============================================================
|
|
81
|
+
# 屏幕操作
|
|
82
|
+
# ============================================================
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def screen_on(self) -> None:
|
|
86
|
+
"""亮屏(唤醒屏幕)。"""
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def screen_off(self) -> None:
|
|
90
|
+
"""熄屏。"""
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def is_screen_on(self) -> bool:
|
|
94
|
+
"""屏幕是否点亮。"""
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def is_screen_locked(self) -> bool:
|
|
98
|
+
"""屏幕是否锁屏。"""
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def unlock(self) -> None:
|
|
102
|
+
"""解锁设备(亮屏 + 上滑/回车解除锁屏)。"""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def set_sleep_time(self, seconds: float) -> None:
|
|
106
|
+
"""设置熄屏时间(秒)。"""
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def restore_sleep_time(self) -> None:
|
|
110
|
+
"""恢复默认熄屏时间。"""
|
|
111
|
+
|
|
112
|
+
# ============================================================
|
|
113
|
+
# 选择器入口(U2 风格)
|
|
114
|
+
# ============================================================
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def __call__(self, **kwargs) -> 'BaseComponent':
|
|
118
|
+
"""U2 风格选择器:d(text=, resourceId=, className=),返回控件对象。
|
|
119
|
+
|
|
120
|
+
平台实现返回具体子类(协变),如 HarmonyDriver 返回 UiObject。
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def xpath(self, xpath: str) -> 'BaseComponent':
|
|
125
|
+
"""xpath 选择器,返回控件对象。"""
|
|
126
|
+
|
|
127
|
+
# ============================================================
|
|
128
|
+
# 应用管理
|
|
129
|
+
# ============================================================
|
|
130
|
+
|
|
131
|
+
@abstractmethod
|
|
132
|
+
def app_start(self, package: str, activity: Optional[str] = None,
|
|
133
|
+
params: str = "", wait_time: float = 1) -> None:
|
|
134
|
+
"""启动应用。"""
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def app_stop(self, package: str, wait_time: float = 0.5) -> None:
|
|
138
|
+
"""停止应用。"""
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def app_current(self) -> Tuple[str, str]:
|
|
142
|
+
"""获取当前前台应用 (package, activity)。"""
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def app_list(self) -> List[str]:
|
|
146
|
+
"""已安装应用列表。"""
|
|
147
|
+
|
|
148
|
+
@abstractmethod
|
|
149
|
+
def has_app(self, package: str) -> bool:
|
|
150
|
+
"""查询是否已安装应用。"""
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def clear_app_data(self, package: str) -> None:
|
|
154
|
+
"""清除应用数据。"""
|
|
155
|
+
|
|
156
|
+
# ============================================================
|
|
157
|
+
# 坐标操作
|
|
158
|
+
# ============================================================
|
|
159
|
+
|
|
160
|
+
@abstractmethod
|
|
161
|
+
def click(self, x: int, y: int) -> None:
|
|
162
|
+
"""坐标点击。"""
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def long_click(self, x: int, y: int, duration: float = 0.5) -> None:
|
|
166
|
+
"""坐标长按。"""
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def double_click(self, x: int, y: int) -> None:
|
|
170
|
+
"""坐标双击。"""
|
|
171
|
+
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def swipe(self, x1: int, y1: int, x2: int, y2: int,
|
|
174
|
+
duration: float = 0.5) -> None:
|
|
175
|
+
"""精确滑动(起止坐标)。"""
|
|
176
|
+
|
|
177
|
+
@abstractmethod
|
|
178
|
+
def swipe_dir(self, direction: str, distance: int = 60,
|
|
179
|
+
area=None, speed=None) -> None:
|
|
180
|
+
"""方向滑动:'UP' / 'DOWN' / 'LEFT' / 'RIGHT'。"""
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def drag(self, x1: int, y1: int, x2: int, y2: int,
|
|
184
|
+
duration: float = 0.5) -> None:
|
|
185
|
+
"""拖拽。"""
|
|
186
|
+
|
|
187
|
+
@abstractmethod
|
|
188
|
+
def fling(self, direction: str, distance: int = 50,
|
|
189
|
+
area=None, speed: str = "fast") -> None:
|
|
190
|
+
"""抛滑(快速惯性滑动)。"""
|
|
191
|
+
|
|
192
|
+
# ============================================================
|
|
193
|
+
# 实时触控(支持 down/move/up 序列,用于实时拖拽等场景)
|
|
194
|
+
# ============================================================
|
|
195
|
+
|
|
196
|
+
@abstractmethod
|
|
197
|
+
def touch_down(self, x: int, y: int) -> None:
|
|
198
|
+
"""按下触控点。"""
|
|
199
|
+
|
|
200
|
+
@abstractmethod
|
|
201
|
+
def touch_move(self, x: int, y: int) -> None:
|
|
202
|
+
"""移动触控点(需先 touch_down)。"""
|
|
203
|
+
|
|
204
|
+
@abstractmethod
|
|
205
|
+
def touch_up(self, x: int, y: int) -> None:
|
|
206
|
+
"""抬起触控点(结束触控序列)。"""
|
|
207
|
+
|
|
208
|
+
# ============================================================
|
|
209
|
+
# 按键
|
|
210
|
+
# ============================================================
|
|
211
|
+
|
|
212
|
+
@abstractmethod
|
|
213
|
+
def press(self, key: str) -> None:
|
|
214
|
+
"""语义按键:'back' / 'home' / 'power' / 'volume_up' / ..."""
|
|
215
|
+
|
|
216
|
+
@abstractmethod
|
|
217
|
+
def press_keycode(self, keycode: int) -> None:
|
|
218
|
+
"""按键码(平台原始按键码,由平台实现自行解释)。"""
|
|
219
|
+
|
|
220
|
+
@abstractmethod
|
|
221
|
+
def go_home(self) -> None:
|
|
222
|
+
"""返回桌面(自动选最稳定路径)。"""
|
|
223
|
+
|
|
224
|
+
@abstractmethod
|
|
225
|
+
def go_back(self) -> None:
|
|
226
|
+
"""返回上一级(自动选最稳定路径)。"""
|
|
227
|
+
|
|
228
|
+
@abstractmethod
|
|
229
|
+
def press_power(self) -> None:
|
|
230
|
+
"""按下电源键。"""
|
|
231
|
+
|
|
232
|
+
@abstractmethod
|
|
233
|
+
def press_combination_key(self, key1: int, key2: int,
|
|
234
|
+
key3: Optional[int] = None) -> None:
|
|
235
|
+
"""按下组合键(支持 2 键或 3 键)。"""
|
|
236
|
+
|
|
237
|
+
# ============================================================
|
|
238
|
+
# Shell
|
|
239
|
+
# ============================================================
|
|
240
|
+
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def shell(self, cmd: str, timeout: float = 60) -> str:
|
|
243
|
+
"""执行 shell 命令,返回回显内容。"""
|
|
244
|
+
|
|
245
|
+
# ============================================================
|
|
246
|
+
# 截图
|
|
247
|
+
# ============================================================
|
|
248
|
+
|
|
249
|
+
@abstractmethod
|
|
250
|
+
def screenshot(self, filename: Optional[str] = None,
|
|
251
|
+
area=None) -> Union['Image', str, None]:
|
|
252
|
+
"""截图。
|
|
253
|
+
|
|
254
|
+
- filename 为 None:返回 PIL.Image.Image
|
|
255
|
+
- filename 指定:保存到文件,返回文件路径 str
|
|
256
|
+
- area 指定区域截图(Rect / SelectorSpec / None)
|
|
257
|
+
- 失败:返回 None
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
@abstractmethod
|
|
261
|
+
def dump_hierarchy(self, source: str = "rpc",
|
|
262
|
+
filename: Optional[str] = None) -> Union[dict, str, None]:
|
|
263
|
+
"""导出控件树。
|
|
264
|
+
|
|
265
|
+
- source: 获取方式
|
|
266
|
+
- "rpc": 走 uitest RPC(Captures.captureLayout),直接返回控件树 JSON,默认
|
|
267
|
+
- "hdc": 走 hdc shell(uitest dumpLayout -p),独立于 RPC 守护进程状态
|
|
268
|
+
- filename 为 None:返回解析后的 dict
|
|
269
|
+
- filename 指定:保存 JSON 到文件,返回文件路径 str
|
|
270
|
+
- 失败:返回 None
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
# ============================================================
|
|
274
|
+
# 等待
|
|
275
|
+
# ============================================================
|
|
276
|
+
|
|
277
|
+
@abstractmethod
|
|
278
|
+
def wait(self, seconds: float) -> None:
|
|
279
|
+
"""强制等待指定秒数。"""
|
|
280
|
+
|
|
281
|
+
@abstractmethod
|
|
282
|
+
def wait_for_idle(self, idle_time: float = 0.7,
|
|
283
|
+
timeout: float = 10) -> None:
|
|
284
|
+
"""等待 UI 进入空闲状态。"""
|
|
285
|
+
|
|
286
|
+
@abstractmethod
|
|
287
|
+
def implicitly_wait(self, seconds: float) -> None:
|
|
288
|
+
"""设置隐式等待。"""
|
|
289
|
+
|
|
290
|
+
# ============================================================
|
|
291
|
+
# 窗口
|
|
292
|
+
# ============================================================
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
@abstractmethod
|
|
296
|
+
def window(self) -> 'BaseWindow':
|
|
297
|
+
"""当前窗口对象。"""
|
|
298
|
+
|
|
299
|
+
@abstractmethod
|
|
300
|
+
def get_windows(self) -> List['BaseWindow']:
|
|
301
|
+
"""获取所有窗口。"""
|
|
302
|
+
|
|
303
|
+
# ============================================================
|
|
304
|
+
# webview 自动化(可选实现,不支持时抛 NotImplementedError)
|
|
305
|
+
# ============================================================
|
|
306
|
+
|
|
307
|
+
def webview(self, bundle_name: str, **kwargs):
|
|
308
|
+
"""连接应用 webview,返回 selenium webdriver 封装。
|
|
309
|
+
|
|
310
|
+
平台不支持 webview 测试时抛 NotImplementedError。
|
|
311
|
+
"""
|
|
312
|
+
raise NotImplementedError("当前平台不支持 webview 自动化")
|
|
313
|
+
|
|
314
|
+
# ============================================================
|
|
315
|
+
# 文件操作
|
|
316
|
+
# ============================================================
|
|
317
|
+
|
|
318
|
+
@abstractmethod
|
|
319
|
+
def push_file(self, local_path: str, remote_path: str,
|
|
320
|
+
timeout: int = 60) -> None:
|
|
321
|
+
"""推送文件到设备。"""
|
|
322
|
+
|
|
323
|
+
@abstractmethod
|
|
324
|
+
def pull_file(self, remote_path: str,
|
|
325
|
+
local_path: Optional[str] = None,
|
|
326
|
+
timeout: int = 60) -> None:
|
|
327
|
+
"""从设备拉取文件。"""
|
|
328
|
+
|
|
329
|
+
@abstractmethod
|
|
330
|
+
def has_file(self, path: str) -> bool:
|
|
331
|
+
"""查询设备端文件是否存在。"""
|
|
332
|
+
|
|
333
|
+
# ============================================================
|
|
334
|
+
# 控件查找
|
|
335
|
+
# ============================================================
|
|
336
|
+
|
|
337
|
+
@abstractmethod
|
|
338
|
+
def find_component(self, target, scroll_target=None) -> Optional['BaseComponent']:
|
|
339
|
+
"""查找控件返回对象。"""
|
|
340
|
+
|
|
341
|
+
@abstractmethod
|
|
342
|
+
def find_all_components(self, target) -> List['BaseComponent']:
|
|
343
|
+
"""查找所有匹配控件。"""
|
|
344
|
+
|
|
345
|
+
@abstractmethod
|
|
346
|
+
def get_component_bound(self, target) -> Optional[Any]:
|
|
347
|
+
"""获取控件边界 Rect,未找到返回 None。"""
|
|
348
|
+
|
|
349
|
+
@abstractmethod
|
|
350
|
+
def get_component_property(self, target, name: str) -> Any:
|
|
351
|
+
"""获取控件指定属性(id/text/key/type/enabled/focused/clickable/scrollable/checked/checkable)。"""
|
|
352
|
+
|
|
353
|
+
# ============================================================
|
|
354
|
+
# 图片识别
|
|
355
|
+
# ============================================================
|
|
356
|
+
|
|
357
|
+
@abstractmethod
|
|
358
|
+
def find_image(self, image_path: str,
|
|
359
|
+
mode: str = "sift") -> Optional[Any]:
|
|
360
|
+
"""在屏幕上查找图片位置,返回 Rect 或 None。"""
|
|
361
|
+
|
|
362
|
+
@abstractmethod
|
|
363
|
+
def touch_image(self, image_path: str, mode: str = "normal",
|
|
364
|
+
similarity: float = 0.95) -> None:
|
|
365
|
+
"""点击图片位置。"""
|
|
366
|
+
|
|
367
|
+
# ============================================================
|
|
368
|
+
# 文本输入辅助
|
|
369
|
+
# ============================================================
|
|
370
|
+
|
|
371
|
+
@abstractmethod
|
|
372
|
+
def hide_keyboard(self) -> None:
|
|
373
|
+
"""隐藏软键盘。"""
|
|
374
|
+
|
|
375
|
+
@abstractmethod
|
|
376
|
+
def input_text_on_cursor(self, text: str) -> None:
|
|
377
|
+
"""在当前光标处输入文本(不依赖控件定位)。"""
|
|
378
|
+
|
|
379
|
+
@abstractmethod
|
|
380
|
+
def move_cursor(self, direction: str, times: int = 1) -> None:
|
|
381
|
+
"""移动输入框光标:'LEFT' / 'RIGHT' / 'UP' / 'DOWN' / 'BEGIN' / 'END'。"""
|
|
382
|
+
|
|
383
|
+
# ============================================================
|
|
384
|
+
# 手势扩展
|
|
385
|
+
# ============================================================
|
|
386
|
+
|
|
387
|
+
@abstractmethod
|
|
388
|
+
def inject_gesture(self, gesture, speed: int = 2000) -> None:
|
|
389
|
+
"""自定义手势。"""
|
|
390
|
+
|
|
391
|
+
# ============================================================
|
|
392
|
+
# 手势导航
|
|
393
|
+
# ============================================================
|
|
394
|
+
|
|
395
|
+
@abstractmethod
|
|
396
|
+
def swipe_to_home(self, times: int = 1) -> None:
|
|
397
|
+
"""屏幕底端上滑回到桌面(需开启手势导航)。"""
|
|
398
|
+
|
|
399
|
+
@abstractmethod
|
|
400
|
+
def swipe_to_back(self, side: str = "LEFT", times: int = 1,
|
|
401
|
+
height: float = 0.5) -> None:
|
|
402
|
+
"""侧滑返回:side='LEFT'/'RIGHT',height 为屏幕高度比例。"""
|
|
403
|
+
|
|
404
|
+
@abstractmethod
|
|
405
|
+
def swipe_to_recent_task(self) -> None:
|
|
406
|
+
"""底端上滑停顿进入多任务界面。"""
|
|
407
|
+
|
|
408
|
+
# ============================================================
|
|
409
|
+
# 坐标转换
|
|
410
|
+
# ============================================================
|
|
411
|
+
|
|
412
|
+
@abstractmethod
|
|
413
|
+
def to_abs_pos(self, x: float, y: float) -> Tuple[int, int]:
|
|
414
|
+
"""比例坐标转绝对坐标。"""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""BaseWindow:跨平台窗口对象契约。"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Any, Dict, List, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseWindow(ABC):
|
|
11
|
+
"""窗口契约,平台实现需继承并实现全部抽象方法。"""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def size(self) -> Tuple[int, int]:
|
|
16
|
+
"""窗口大小 (width, height)。"""
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def info(self) -> Dict[str, Any]:
|
|
21
|
+
"""窗口信息。"""
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get_windows(self) -> List['BaseWindow']:
|
|
25
|
+
"""所有窗口对象列表。"""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""SelectorSpec:纯数据选择器规格。
|
|
4
|
+
|
|
5
|
+
对齐 U2 单对象模型:d(**kwargs) 直接返回 UiObject,SelectorSpec 仅作为
|
|
6
|
+
内部纯数据类封装控件定位条件,不向用户暴露任何操作接口。所有操作与
|
|
7
|
+
关系方法统一在 UiObject / BaseComponent 上。
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SelectorSpec:
|
|
17
|
+
"""控件定位条件的纯数据封装(内部使用,不直接暴露给用户)。
|
|
18
|
+
|
|
19
|
+
用户通过 d(text="x", resourceId="y") 构造,由 UiObject 持有。
|
|
20
|
+
frozen=True 保证不可变,关系选择器衍生新实例时安全。
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# 文本匹配
|
|
24
|
+
text: Optional[str] = None
|
|
25
|
+
text_contains: Optional[str] = None
|
|
26
|
+
text_starts_with: Optional[str] = None
|
|
27
|
+
text_ends_with: Optional[str] = None
|
|
28
|
+
text_matches: Optional[str] = None
|
|
29
|
+
text_matches_flags: Optional[int] = None
|
|
30
|
+
|
|
31
|
+
# 描述匹配
|
|
32
|
+
desc: Optional[str] = None
|
|
33
|
+
desc_contains: Optional[str] = None
|
|
34
|
+
desc_starts_with: Optional[str] = None
|
|
35
|
+
desc_ends_with: Optional[str] = None
|
|
36
|
+
desc_matches: Optional[str] = None
|
|
37
|
+
|
|
38
|
+
# 通用定位
|
|
39
|
+
resource_id: Optional[str] = None # 对应 U2 resourceId / id
|
|
40
|
+
class_name: Optional[str] = None # 对应 U2 className / class_
|
|
41
|
+
key: Optional[str] = None # 鸿蒙 key 选择器
|
|
42
|
+
type: Optional[str] = None # 鸿蒙 type 选择器
|
|
43
|
+
index: Optional[int] = None
|
|
44
|
+
instance: Optional[int] = None
|
|
45
|
+
|
|
46
|
+
# 关系选择器
|
|
47
|
+
parent: Optional['SelectorSpec'] = None
|
|
48
|
+
relation: Optional[str] = None # 'child' / 'sibling' / 'after' / 'before'
|
|
49
|
+
|
|
50
|
+
# xpath
|
|
51
|
+
xpath: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
def merge(self, **kwargs) -> 'SelectorSpec':
|
|
54
|
+
"""合并新的选择条件,返回新实例(不可变)。"""
|
|
55
|
+
normalized = _normalize_aliases(kwargs)
|
|
56
|
+
merged = {**self.__dict__, **normalized}
|
|
57
|
+
return SelectorSpec(**merged)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def with_relation(cls, parent: 'SelectorSpec', relation: str,
|
|
61
|
+
child: 'SelectorSpec') -> 'SelectorSpec':
|
|
62
|
+
"""构造关系选择器,避免 parent/relation 重复传参。"""
|
|
63
|
+
child_data = {
|
|
64
|
+
key: value
|
|
65
|
+
for key, value in child.__dict__.items()
|
|
66
|
+
if key not in ('parent', 'relation')
|
|
67
|
+
}
|
|
68
|
+
return cls(parent=parent, relation=relation, **child_data)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# U2 / 鸿蒙选择器别名 → 内部字段名
|
|
72
|
+
_ALIAS_MAPPING = {
|
|
73
|
+
'className': 'class_name',
|
|
74
|
+
'class_': 'class_name',
|
|
75
|
+
'resourceId': 'resource_id',
|
|
76
|
+
'id': 'resource_id',
|
|
77
|
+
'description': 'desc',
|
|
78
|
+
'textContains': 'text_contains',
|
|
79
|
+
'textStartswith': 'text_starts_with',
|
|
80
|
+
'textStartsWith': 'text_starts_with',
|
|
81
|
+
'textEndswith': 'text_ends_with',
|
|
82
|
+
'textEndsWith': 'text_ends_with',
|
|
83
|
+
'textMatches': 'text_matches',
|
|
84
|
+
'flags': 'text_matches_flags',
|
|
85
|
+
'descContains': 'desc_contains',
|
|
86
|
+
'descStartswith': 'desc_starts_with',
|
|
87
|
+
'descStartsWith': 'desc_starts_with',
|
|
88
|
+
'descEndswith': 'desc_ends_with',
|
|
89
|
+
'descEndsWith': 'desc_ends_with',
|
|
90
|
+
'descMatches': 'desc_matches',
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _normalize_aliases(kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
|
95
|
+
"""U2 / 鸿蒙选择器别名归一化为内部字段。"""
|
|
96
|
+
return {_ALIAS_MAPPING.get(key, key): value for key, value in kwargs.items()}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_selector(**kwargs) -> SelectorSpec:
|
|
100
|
+
"""从用户 kwargs 构建 SelectorSpec(含别名归一化)。"""
|
|
101
|
+
normalized = _normalize_aliases(kwargs)
|
|
102
|
+
return SelectorSpec(**normalized)
|
devhelmkit/entry.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""设备连接入口:平台识别、设备发现与平台分发。
|
|
4
|
+
|
|
5
|
+
平台实现延迟导入,避免导入包时触发设备环境探测。
|
|
6
|
+
"""
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from devhelmkit.core.base_driver import BaseDriver
|
|
10
|
+
from devhelmkit.exceptions import (
|
|
11
|
+
DeviceConnectError,
|
|
12
|
+
DeviceNotFoundError,
|
|
13
|
+
PlatformNotSupportedError,
|
|
14
|
+
)
|
|
15
|
+
from devhelmkit.utils.logger import setup_logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def connect(
|
|
19
|
+
serial: Optional[str] = None,
|
|
20
|
+
platform: str = "auto",
|
|
21
|
+
config=None,
|
|
22
|
+
log_level: int = None,
|
|
23
|
+
**kwargs
|
|
24
|
+
) -> BaseDriver:
|
|
25
|
+
"""连接设备。
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
serial: 设备序列号,None 时自动发现。
|
|
29
|
+
platform: 平台,"auto" | "harmony" | "android"。
|
|
30
|
+
config: 平台专用配置对象(如 HarmonyDriverConfig)。
|
|
31
|
+
log_level: 日志级别,None 默认 INFO。传入 logging.DEBUG 可开启调试日志。
|
|
32
|
+
**kwargs: 平台特定参数,覆盖 config 同名字段。
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
BaseDriver: 设备驱动实例(具体平台子类)。
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
DeviceNotFoundError: 未检测到可连接设备。
|
|
39
|
+
DeviceConnectError: 设备连接失败。
|
|
40
|
+
PlatformNotSupportedError: 平台不支持。
|
|
41
|
+
"""
|
|
42
|
+
import logging
|
|
43
|
+
setup_logger(log_level if log_level is not None else logging.INFO)
|
|
44
|
+
|
|
45
|
+
if serial is None:
|
|
46
|
+
serial = _discover_serial()
|
|
47
|
+
|
|
48
|
+
selected_platform = _detect_platform(serial) if platform == "auto" else platform
|
|
49
|
+
|
|
50
|
+
if selected_platform == "harmony":
|
|
51
|
+
from devhelmkit.harmony.driver import HarmonyDriver
|
|
52
|
+
return HarmonyDriver(serial, config=config, **kwargs)
|
|
53
|
+
|
|
54
|
+
if selected_platform == "android":
|
|
55
|
+
raise PlatformNotSupportedError("Android 平台暂未接入,阶段四实现")
|
|
56
|
+
|
|
57
|
+
raise PlatformNotSupportedError("不支持的平台: %s" % selected_platform)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _discover_serial() -> str:
|
|
61
|
+
"""自动发现首个可用设备序列号。"""
|
|
62
|
+
try:
|
|
63
|
+
from devhelmkit.harmony.device.hdc import HdcDevice
|
|
64
|
+
targets = HdcDevice.list_targets()
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
raise DeviceConnectError("鸿蒙设备发现失败: %s" % exc) from exc
|
|
67
|
+
|
|
68
|
+
if not targets:
|
|
69
|
+
raise DeviceNotFoundError(
|
|
70
|
+
"未检测到可连接设备,请手动指定 serial 或 platform"
|
|
71
|
+
)
|
|
72
|
+
return targets[0]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _detect_platform(serial: Optional[str]) -> str:
|
|
76
|
+
"""自动识别平台:当前仅识别鸿蒙设备。"""
|
|
77
|
+
try:
|
|
78
|
+
from devhelmkit.harmony.device.hdc import HdcDevice
|
|
79
|
+
targets = HdcDevice.list_targets()
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
raise DeviceConnectError("鸿蒙设备发现失败: %s" % exc) from exc
|
|
82
|
+
|
|
83
|
+
if serial is None and targets:
|
|
84
|
+
return "harmony"
|
|
85
|
+
if serial is not None and serial in targets:
|
|
86
|
+
return "harmony"
|
|
87
|
+
|
|
88
|
+
raise DeviceNotFoundError(
|
|
89
|
+
"未检测到可连接设备,请手动指定 serial 或 platform"
|
|
90
|
+
)
|
devhelmkit/exceptions.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""统一异常体系。
|
|
4
|
+
|
|
5
|
+
异常类型单文件定义,避免各平台重复定义。命名约束:
|
|
6
|
+
- DevhelmTimeoutError 不与 Python 内建 TimeoutError 同名冲突;
|
|
7
|
+
- DeviceConnectError 作为设备连接失败的唯一命名;
|
|
8
|
+
- ComponentNotFoundError 作为控件未找到的唯一命名。
|
|
9
|
+
"""
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DevhelmError(Exception):
|
|
14
|
+
"""框架基础异常,所有 devhelmkit 异常的根。"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DeviceNotFoundError(DevhelmError):
|
|
18
|
+
"""未检测到可连接设备。"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeviceConnectError(DevhelmError):
|
|
22
|
+
"""设备连接失败(hdc 连接、RPC socket 建立失败等)。"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PlatformNotSupportedError(DevhelmError):
|
|
26
|
+
"""平台不支持(如阶段一调用 connect(platform="android"))。"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DevhelmTimeoutError(DevhelmError):
|
|
30
|
+
"""框架操作超时,刻意不与 Python 内建 TimeoutError 同名。"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RpcError(DevhelmError):
|
|
34
|
+
"""RPC 通信异常,携带 api 与原始 reply 上下文便于诊断。"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str, api: Optional[str] = None,
|
|
37
|
+
reply: Optional[str] = None):
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
self.api = api
|
|
40
|
+
self.reply = reply
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BackendObjectDroppedError(RpcError):
|
|
44
|
+
"""设备端远程对象引用已失效,需恢复后重试。"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ComponentNotFoundError(DevhelmError):
|
|
48
|
+
"""控件未找到。"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ComponentDisappearedError(ComponentNotFoundError):
|
|
52
|
+
"""控件查找到后又消失。"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AgentError(DevhelmError):
|
|
56
|
+
"""设备端 uitest / Agent 异常(进程崩溃、版本不支持等)。"""
|