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.
Files changed (58) hide show
  1. devhelmkit/__init__.py +44 -0
  2. devhelmkit/android/__init__.py +7 -0
  3. devhelmkit/android/driver.py +18 -0
  4. devhelmkit/assets/so/arm64-v8a/agent_v1.so +0 -0
  5. devhelmkit/assets/so/arm64-v8a/agent_v2.so +0 -0
  6. devhelmkit/assets/so/x86_64/agent.so +0 -0
  7. devhelmkit/core/__init__.py +3 -0
  8. devhelmkit/core/base_component.py +214 -0
  9. devhelmkit/core/base_driver.py +414 -0
  10. devhelmkit/core/base_window.py +25 -0
  11. devhelmkit/core/selector_spec.py +102 -0
  12. devhelmkit/entry.py +90 -0
  13. devhelmkit/exceptions.py +56 -0
  14. devhelmkit/harmony/__init__.py +3 -0
  15. devhelmkit/harmony/agent/__init__.py +7 -0
  16. devhelmkit/harmony/agent/so_manager.py +300 -0
  17. devhelmkit/harmony/config.py +124 -0
  18. devhelmkit/harmony/device/__init__.py +6 -0
  19. devhelmkit/harmony/device/hdc.py +430 -0
  20. devhelmkit/harmony/driver.py +1416 -0
  21. devhelmkit/harmony/finder/__init__.py +12 -0
  22. devhelmkit/harmony/finder/component_finder.py +336 -0
  23. devhelmkit/harmony/finder/popup_handler.py +81 -0
  24. devhelmkit/harmony/finder/selector_adapter.py +101 -0
  25. devhelmkit/harmony/finder/xpath_query.py +110 -0
  26. devhelmkit/harmony/rpc/__init__.py +12 -0
  27. devhelmkit/harmony/rpc/client.py +126 -0
  28. devhelmkit/harmony/rpc/proxy_v2.py +106 -0
  29. devhelmkit/harmony/rpc/remote_object.py +48 -0
  30. devhelmkit/harmony/uiobject.py +246 -0
  31. devhelmkit/harmony/uiwindow.py +43 -0
  32. devhelmkit/harmony/webview/__init__.py +17 -0
  33. devhelmkit/harmony/webview/chromedriver_manager.py +251 -0
  34. devhelmkit/harmony/webview/devtools_finder.py +131 -0
  35. devhelmkit/harmony/webview/webview_driver.py +326 -0
  36. devhelmkit/model/__init__.py +3 -0
  37. devhelmkit/model/action.py +147 -0
  38. devhelmkit/model/app_state.py +50 -0
  39. devhelmkit/model/constants.py +22 -0
  40. devhelmkit/model/display.py +32 -0
  41. devhelmkit/model/format_string.py +24 -0
  42. devhelmkit/model/input.py +50 -0
  43. devhelmkit/model/json_base.py +42 -0
  44. devhelmkit/model/keys.py +375 -0
  45. devhelmkit/model/match_pattern.py +15 -0
  46. devhelmkit/model/page.py +13 -0
  47. devhelmkit/model/params.py +42 -0
  48. devhelmkit/model/rect.py +58 -0
  49. devhelmkit/model/runnable.py +18 -0
  50. devhelmkit/utils/__init__.py +3 -0
  51. devhelmkit/utils/logger.py +72 -0
  52. devhelmkit/utils/retry.py +46 -0
  53. devhelmkit/utils/timeout.py +64 -0
  54. devhelmkit-0.1.0.dist-info/METADATA +411 -0
  55. devhelmkit-0.1.0.dist-info/RECORD +58 -0
  56. devhelmkit-0.1.0.dist-info/WHEEL +5 -0
  57. devhelmkit-0.1.0.dist-info/licenses/LICENSE +201 -0
  58. devhelmkit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,12 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """鸿蒙控件查找后端。
4
+
5
+ 子模块:
6
+ - selector_adapter:SelectorSpec → 设备端 On 查询条件转换
7
+ - component_finder:ComponentFinder 统一查找门面
8
+ - popup_handler:弹窗自动消除策略
9
+
10
+ 仅支持 HarmonyOS 5.0.0+(API 12+),设备端 uitest 服务统一使用
11
+ api9+ 命名(Driver/Component/On)。
12
+ """
@@ -0,0 +1,336 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """ComponentFinder:鸿蒙控件查找统一门面。
4
+
5
+ 职责:
6
+ - 将 SelectorSpec 转为设备端 By/On 链(通过 selector_adapter + RPC)
7
+ - 调用 Driver.findComponent / Driver.waitForComponent 获取 Component 引用
8
+ - 在 Component 引用上执行操作方法
9
+ - 弹窗自动消除(配置开启时,查找失败自动尝试消除弹窗后重试)
10
+
11
+ By/On 链构造流程:
12
+ On#seed (静态根)
13
+ → On.text("xx", pattern) → By#1
14
+ → On.id("yy") → By#2 (this=By#1)
15
+ → On.within(parent_ref) → By#3 (this=By#2, args=[parent_ref])
16
+
17
+ 关系选择器递归处理:先构造 parent By 链,再在 child By 链末尾
18
+ 调用 On.within / On.isAfter / On.isBefore。
19
+ """
20
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
21
+
22
+ from devhelmkit.core.selector_spec import SelectorSpec, build_selector
23
+ from devhelmkit.exceptions import ComponentNotFoundError, RpcError
24
+ from devhelmkit.harmony.finder.selector_adapter import (
25
+ RELATION_API,
26
+ selector_to_by_chain,
27
+ )
28
+ from devhelmkit.harmony.finder.xpath_query import query_xpath
29
+ from devhelmkit.harmony.finder.popup_handler import PopupHandler
30
+ from devhelmkit.utils import logger
31
+
32
+ if TYPE_CHECKING:
33
+ from devhelmkit.harmony.config import HarmonyDriverConfig
34
+ from devhelmkit.harmony.rpc.client import RpcClient
35
+
36
+ # 设备端静态对象引用
37
+ DRIVER_REF = "Driver#0"
38
+ ON_SEED_REF = "On#seed"
39
+
40
+ # 默认查找超时(秒)
41
+ DEFAULT_FIND_TIMEOUT = 10.0
42
+
43
+
44
+ class ComponentFinder:
45
+ """鸿蒙控件查找门面,统一对接设备端 uitest 服务。"""
46
+
47
+ def __init__(self, rpc: 'RpcClient', config: 'HarmonyDriverConfig'):
48
+ self._rpc = rpc
49
+ self._config = config
50
+ self._popup_handler = PopupHandler(
51
+ self.find_component, rpc, config
52
+ )
53
+
54
+ # ============================================================
55
+ # 控件查找
56
+ # ============================================================
57
+
58
+ def find_component(self, selector: SelectorSpec,
59
+ timeout: float = DEFAULT_FIND_TIMEOUT) -> str:
60
+ """查找单个控件,返回 Component 对象引用。
61
+
62
+ Args:
63
+ selector: 控件定位条件
64
+ timeout: 查找超时(秒)
65
+
66
+ Returns:
67
+ 设备端 Component 对象引用(如 "Component#3")
68
+
69
+ Raises:
70
+ ComponentNotFoundError: 控件未找到
71
+ RpcError: RPC 通信异常
72
+ """
73
+ # xpath 选择器走客户端查询:dump 控件树 → 查询节点 → 降级 SelectorSpec → By 链
74
+ if selector.xpath is not None:
75
+ return self._find_by_xpath(selector, timeout)
76
+ by_ref = self._build_by_ref(selector)
77
+ timeout_ms = int(timeout * 1000)
78
+ try:
79
+ result = self._rpc.call(
80
+ "Driver.waitForComponent", DRIVER_REF, [by_ref, timeout_ms]
81
+ )
82
+ except RpcError as e:
83
+ if self._should_dismiss_popup():
84
+ logger.debug("控件查找失败,尝试消除弹窗后重试: %s",
85
+ _format_selector(selector))
86
+ if self._popup_handler.dismiss_popups():
87
+ result = self._rpc.call(
88
+ "Driver.waitForComponent", DRIVER_REF,
89
+ [by_ref, timeout_ms]
90
+ )
91
+ else:
92
+ raise ComponentNotFoundError(
93
+ "控件未找到: %s" % _format_selector(selector)
94
+ ) from e
95
+ else:
96
+ raise
97
+ if not result:
98
+ raise ComponentNotFoundError(
99
+ "控件未找到: %s" % _format_selector(selector)
100
+ )
101
+ return result
102
+
103
+ def find_components(self, selector: SelectorSpec) -> list:
104
+ """查找所有匹配控件,返回 Component 引用列表。
105
+
106
+ 不等待,立即返回当前匹配结果。
107
+
108
+ Args:
109
+ selector: 控件定位条件
110
+
111
+ Returns:
112
+ Component 对象引用列表(可能为空)
113
+ """
114
+ if selector.xpath is not None:
115
+ return self._find_components_by_xpath(selector)
116
+ by_ref = self._build_by_ref(selector)
117
+ result = self._rpc.call("Driver.findComponents", DRIVER_REF, [by_ref])
118
+ return result if isinstance(result, list) else []
119
+
120
+ def component_exists(self, selector: SelectorSpec) -> bool:
121
+ """检查控件是否存在。
122
+
123
+ xpath 选择器直接在控件树上查询,无需构造 By 链。
124
+ """
125
+ if selector.xpath is not None:
126
+ return bool(self._query_xpath_nodes(selector))
127
+ return bool(self.find_components(selector))
128
+
129
+ def wait_component(self, selector: SelectorSpec,
130
+ timeout: float) -> bool:
131
+ """等待控件出现,返回是否出现。"""
132
+ try:
133
+ self.find_component(selector, timeout)
134
+ return True
135
+ except ComponentNotFoundError:
136
+ return False
137
+
138
+ def wait_component_gone(self, selector: SelectorSpec,
139
+ timeout: float) -> bool:
140
+ """等待控件消失,返回是否消失。"""
141
+ import time
142
+ deadline = time.monotonic() + timeout
143
+ while time.monotonic() < deadline:
144
+ if not self.component_exists(selector):
145
+ return True
146
+ time.sleep(0.5)
147
+ return not self.component_exists(selector)
148
+
149
+ # ============================================================
150
+ # 控件操作
151
+ # ============================================================
152
+
153
+ def call_component(self, selector: SelectorSpec, method: str,
154
+ args: Optional[list] = None,
155
+ timeout: float = DEFAULT_FIND_TIMEOUT) -> Any:
156
+ """查找控件并调用其方法。
157
+
158
+ Args:
159
+ selector: 控件定位条件
160
+ method: Component 方法名(如 "click"、"getText")
161
+ args: 方法参数列表
162
+ timeout: 查找超时
163
+
164
+ Returns:
165
+ 方法返回值
166
+ """
167
+ component_ref = self.find_component(selector, timeout)
168
+ return self._rpc.call("Component." + method, component_ref, args or [])
169
+
170
+ def call_component_by_ref(self, component_ref: str, method: str,
171
+ args: Optional[list] = None) -> Any:
172
+ """直接在 Component 引用上调用方法(跳过查找)。
173
+
174
+ 用于已持有 Component 引用的场景,避免重复查找。
175
+ """
176
+ return self._rpc.call("Component." + method, component_ref, args or [])
177
+
178
+ # ============================================================
179
+ # By/On 链构造
180
+ # ============================================================
181
+
182
+ def _build_by_ref(self, selector: SelectorSpec) -> str:
183
+ """通过 RPC 顺序调用构造 By/On 链,返回 By 对象引用。
184
+
185
+ 递归处理关系选择器:先构造 parent By 链,再在 child By 链
186
+ 末尾调用 On.within / On.isAfter / On.isBefore。
187
+ """
188
+ chain = selector_to_by_chain(selector)
189
+ by_ref = ON_SEED_REF
190
+ for api_name, args in chain:
191
+ by_ref = self._rpc.call(api_name, by_ref, args)
192
+
193
+ # 关系选择器:在 child By 链末尾追加关系调用
194
+ if selector.parent is not None and selector.relation:
195
+ parent_ref = self._build_by_ref(selector.parent)
196
+ relation_api = RELATION_API.get(selector.relation)
197
+ if relation_api is not None:
198
+ by_ref = self._rpc.call(relation_api, by_ref, [parent_ref])
199
+ elif selector.relation == 'sibling':
200
+ # TODO: sibling 无直接对应的 On API,需通过 parent + child 间接实现
201
+ pass
202
+
203
+ return by_ref
204
+
205
+ # ============================================================
206
+ # xpath 客户端查询
207
+ # 设备端 uitest 不支持 xpath,通过 Captures.captureLayout 获取
208
+ # 控件树 JSON 后,在客户端执行 xpath 查询,再将匹配节点降级为
209
+ # SelectorSpec 走正常 By 链,复用 Component 引用机制。
210
+ # ============================================================
211
+
212
+ def _dump_layout(self) -> Optional[Dict[str, Any]]:
213
+ """通过 uitest RPC (Captures.captureLayout) 获取控件树。
214
+
215
+ 比 hdc shell dumpLayout 快,无需文件中转。
216
+ """
217
+ import json
218
+ from devhelmkit.harmony.rpc.proxy_v2 import rpc_captures
219
+
220
+ reply = rpc_captures(self._rpc.device, "captureLayout", {})
221
+ try:
222
+ data = json.loads(reply)
223
+ except json.JSONDecodeError as e:
224
+ logger.error("控件树响应解析失败: %s", e)
225
+ return None
226
+ if isinstance(data, dict):
227
+ if data.get('error'):
228
+ logger.warn("captureLayout 调用失败: %s", data['error'])
229
+ return None
230
+ if data.get('exception'):
231
+ msg = data['exception'].get('message', str(data['exception']))
232
+ logger.warn("captureLayout 调用异常: %s", msg)
233
+ return None
234
+ if 'result' in data:
235
+ data = data['result']
236
+ return data if isinstance(data, dict) else None
237
+
238
+ def _query_xpath_nodes(self, selector: SelectorSpec) -> List[Dict[str, Any]]:
239
+ """执行 xpath 查询,返回匹配节点列表。"""
240
+ if not selector.xpath:
241
+ return []
242
+ tree = self._dump_layout()
243
+ if tree is None:
244
+ return []
245
+ return query_xpath(tree, selector.xpath)
246
+
247
+ def _node_to_selector(self, node: Dict[str, Any]) -> SelectorSpec:
248
+ """从控件树节点提取定位条件,构造降级 SelectorSpec。
249
+
250
+ 优先使用唯一性强的属性:key > resource_id > text > type。
251
+ """
252
+ attrs = node.get("attributes") or {}
253
+ kwargs: Dict[str, Any] = {}
254
+ # key 唯一性最强
255
+ key = attrs.get("key")
256
+ if key:
257
+ kwargs["key"] = key
258
+ else:
259
+ resource_id = attrs.get("id") or attrs.get("resource_id")
260
+ if resource_id:
261
+ kwargs["resource_id"] = resource_id
262
+ # text 辅助定位
263
+ text = attrs.get("text")
264
+ if text:
265
+ kwargs["text"] = text
266
+ # type 兜底
267
+ node_type = attrs.get("type")
268
+ if node_type:
269
+ kwargs["type"] = node_type
270
+ return build_selector(**kwargs)
271
+
272
+ def _find_by_xpath(self, selector: SelectorSpec,
273
+ timeout: float) -> str:
274
+ """xpath 选择器查找单个控件。
275
+
276
+ dump 控件树 → 查询匹配节点 → 取第一个节点降级为 SelectorSpec → By 链。
277
+ """
278
+ nodes = self._query_xpath_nodes(selector)
279
+ if not nodes:
280
+ raise ComponentNotFoundError(
281
+ "xpath 未匹配控件: %s" % selector.xpath
282
+ )
283
+ degraded = self._node_to_selector(nodes[0])
284
+ # 递归走正常 By 链(degraded 无 xpath,不会再次进入 xpath 分支)
285
+ return self.find_component(degraded, timeout)
286
+
287
+ def _find_components_by_xpath(self, selector: SelectorSpec) -> list:
288
+ """xpath 选择器查找所有匹配控件。
289
+
290
+ 对每个匹配节点降级为 SelectorSpec,按降级条件去重后逐个 findComponents,
291
+ 收集所有 Component 引用。
292
+ """
293
+ nodes = self._query_xpath_nodes(selector)
294
+ refs: list = []
295
+ seen: set = set()
296
+ for node in nodes:
297
+ degraded = self._node_to_selector(node)
298
+ # 去重:相同降级条件只查一次
299
+ sig = (degraded.type, degraded.text,
300
+ degraded.key, degraded.resource_id)
301
+ if sig in seen:
302
+ continue
303
+ seen.add(sig)
304
+ by_ref = self._build_by_ref(degraded)
305
+ result = self._rpc.call(
306
+ "Driver.findComponents", DRIVER_REF, [by_ref]
307
+ )
308
+ if isinstance(result, list):
309
+ refs.extend(result)
310
+ return refs
311
+
312
+ # ============================================================
313
+ # 弹窗处理
314
+ # ============================================================
315
+
316
+ def _should_dismiss_popup(self) -> bool:
317
+ """判断是否启用弹窗自动消除。"""
318
+ return self._config.pop_window_dismiss == "enable"
319
+
320
+
321
+ def _format_selector(selector: SelectorSpec) -> str:
322
+ """格式化选择器为可读字符串,用于异常信息。"""
323
+ parts = []
324
+ if selector.text is not None:
325
+ parts.append("text=%r" % selector.text)
326
+ if selector.text_contains is not None:
327
+ parts.append("textContains=%r" % selector.text_contains)
328
+ if selector.resource_id is not None:
329
+ parts.append("resourceId=%r" % selector.resource_id)
330
+ if selector.key is not None:
331
+ parts.append("key=%r" % selector.key)
332
+ if selector.class_name is not None:
333
+ parts.append("className=%r" % selector.class_name)
334
+ if selector.xpath is not None:
335
+ parts.append("xpath=%r" % selector.xpath)
336
+ return ", ".join(parts) if parts else str(selector)
@@ -0,0 +1,81 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """弹窗自动消除策略。
4
+
5
+ 在控件查找失败时,自动检测并消除常见系统弹窗(权限请求、更新提示、
6
+ 广告弹窗等),消除后重试查找。
7
+
8
+ 弹窗消除策略:遍历常见确认类按钮文本,查找并点击存在的弹窗按钮。
9
+ 仅点击确认类按钮(允许/确定/知道了),不点击取消类按钮。
10
+
11
+ PopupHandler 不直接依赖 ComponentFinder,通过注入的 find_component
12
+ 回调查找弹窗按钮,避免循环依赖。
13
+ """
14
+ from typing import Callable, TYPE_CHECKING
15
+
16
+ from devhelmkit.core.selector_spec import SelectorSpec, build_selector
17
+ from devhelmkit.exceptions import ComponentNotFoundError, RpcError
18
+ from devhelmkit.utils import logger
19
+
20
+ if TYPE_CHECKING:
21
+ from devhelmkit.harmony.config import HarmonyDriverConfig
22
+ from devhelmkit.harmony.rpc.client import RpcClient
23
+
24
+ # 常见弹窗确认类按钮文本(按优先级排序)
25
+ _POPUP_DISMISS_TEXTS = [
26
+ "允许",
27
+ "仅在使用中允许",
28
+ "始终允许",
29
+ "确定",
30
+ "我知道了",
31
+ "知道了",
32
+ "同意",
33
+ "继续",
34
+ "关闭",
35
+ "跳过",
36
+ ]
37
+
38
+
39
+ class PopupHandler:
40
+ """弹窗自动消除处理器。
41
+
42
+ 通过注入的 find_component 回调查找弹窗按钮,找到后通过 RPC
43
+ 调用 Component.click 点击消除。
44
+ """
45
+
46
+ def __init__(self, find_component: Callable[[SelectorSpec, float], str],
47
+ rpc: 'RpcClient', config: 'HarmonyDriverConfig'):
48
+ self._find_component = find_component
49
+ self._rpc = rpc
50
+ self._config = config
51
+
52
+ def dismiss_popups(self) -> bool:
53
+ """检测并消除弹窗,返回是否处理了弹窗。
54
+
55
+ 遍历常见弹窗按钮文本,查找并点击存在的按钮。
56
+ 最多处理 pop_window_handle_times 个弹窗。
57
+ """
58
+ handled = False
59
+ max_times = self._config.pop_window_handle_times
60
+ for _ in range(max_times):
61
+ if not self._dismiss_one_popup():
62
+ break
63
+ handled = True
64
+ return handled
65
+
66
+ def _dismiss_one_popup(self) -> bool:
67
+ """消除单个弹窗,返回是否找到并处理了弹窗。"""
68
+ for text in _POPUP_DISMISS_TEXTS:
69
+ try:
70
+ component_ref = self._find_component(
71
+ build_selector(text=text), 1.0
72
+ )
73
+ self._rpc.call("Component.click", component_ref, [])
74
+ logger.info("已消除弹窗: %s", text)
75
+ return True
76
+ except ComponentNotFoundError:
77
+ continue
78
+ except RpcError as e:
79
+ logger.warn("消除弹窗时 RPC 异常: %s", e)
80
+ continue
81
+ return False
@@ -0,0 +1,101 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """SelectorSpec → 设备端 On 查询条件转换。
4
+
5
+ 将跨平台纯数据 SelectorSpec 转为设备端 By/On 链构造计划。
6
+ 本模块只做纯数据转换,不执行 RPC 调用,不依赖 RpcClient。
7
+
8
+ By/On 链构造顺序:
9
+ 1. 从静态根 On#seed 开始
10
+ 2. 按 selector_to_by_chain 返回的 [(api_name, args), ...] 顺序 RPC 调用
11
+ 3. 每次调用返回新的 By 对象引用,作为下一次调用的 this
12
+
13
+ 关系选择器(parent/relation)不在本模块处理,由 ComponentFinder
14
+ 在构造 By 链时递归处理 parent 后调用 On.within / On.isAfter / On.isBefore。
15
+ """
16
+ from typing import List, Tuple
17
+
18
+ from devhelmkit.core.selector_spec import SelectorSpec
19
+ from devhelmkit.model.match_pattern import MatchPattern
20
+
21
+
22
+ def selector_to_by_chain(selector: SelectorSpec) -> List[Tuple[str, list]]:
23
+ """将叶子选择器转为 By/On 链构造计划。
24
+
25
+ 不处理关系选择器(parent/relation),仅转换定位条件。
26
+
27
+ Args:
28
+ selector: 控件定位条件
29
+
30
+ Returns:
31
+ [(api_name, args), ...],按顺序 RPC 调用构造 By 链。
32
+ api_name 为 api9+ 风格(On.text / On.id / On.type / On.description)。
33
+ 空列表表示无定位条件(匹配所有控件)。
34
+ """
35
+ chain: List[Tuple[str, list]] = []
36
+
37
+ _append_text_chain(chain, selector)
38
+ _append_desc_chain(chain, selector)
39
+ _append_id_chain(chain, selector)
40
+ _append_type_chain(chain, selector)
41
+
42
+ return chain
43
+
44
+
45
+ def _append_text_chain(chain: List[Tuple[str, list]],
46
+ selector: SelectorSpec) -> None:
47
+ """文本匹配条件,同组互斥,按优先级取第一个非空字段。"""
48
+ if selector.text is not None:
49
+ chain.append(("On.text", [selector.text, int(MatchPattern.EQUALS)]))
50
+ elif selector.text_contains is not None:
51
+ chain.append(("On.text", [selector.text_contains, int(MatchPattern.CONTAINS)]))
52
+ elif selector.text_starts_with is not None:
53
+ chain.append(("On.text", [selector.text_starts_with, int(MatchPattern.STARTS_WITH)]))
54
+ elif selector.text_ends_with is not None:
55
+ chain.append(("On.text", [selector.text_ends_with, int(MatchPattern.ENDS_WITH)]))
56
+ elif selector.text_matches is not None:
57
+ chain.append(("On.text", [selector.text_matches, int(MatchPattern.REGEXP)]))
58
+
59
+
60
+ def _append_desc_chain(chain: List[Tuple[str, list]],
61
+ selector: SelectorSpec) -> None:
62
+ """描述匹配条件,同组互斥。"""
63
+ if selector.desc is not None:
64
+ chain.append(("On.description", [selector.desc, int(MatchPattern.EQUALS)]))
65
+ elif selector.desc_contains is not None:
66
+ chain.append(("On.description", [selector.desc_contains, int(MatchPattern.CONTAINS)]))
67
+ elif selector.desc_starts_with is not None:
68
+ chain.append(("On.description", [selector.desc_starts_with, int(MatchPattern.STARTS_WITH)]))
69
+ elif selector.desc_ends_with is not None:
70
+ chain.append(("On.description", [selector.desc_ends_with, int(MatchPattern.ENDS_WITH)]))
71
+ elif selector.desc_matches is not None:
72
+ chain.append(("On.description", [selector.desc_matches, int(MatchPattern.REGEXP)]))
73
+
74
+
75
+ def _append_id_chain(chain: List[Tuple[str, list]],
76
+ selector: SelectorSpec) -> None:
77
+ """ID/Key 定位,key 优先于 resource_id。
78
+
79
+ API 12+ 中 By.key 和 By.id 统一映射到 On.id。
80
+ """
81
+ if selector.key is not None:
82
+ chain.append(("On.id", [selector.key]))
83
+ elif selector.resource_id is not None:
84
+ chain.append(("On.id", [selector.resource_id]))
85
+
86
+
87
+ def _append_type_chain(chain: List[Tuple[str, list]],
88
+ selector: SelectorSpec) -> None:
89
+ """类型定位,class_name 优先于 type。"""
90
+ if selector.class_name is not None:
91
+ chain.append(("On.type", [selector.class_name]))
92
+ elif selector.type is not None:
93
+ chain.append(("On.type", [selector.type]))
94
+
95
+
96
+ # 关系选择器 → 设备端 On API 映射
97
+ RELATION_API = {
98
+ 'child': 'On.within',
99
+ 'after': 'On.isAfter',
100
+ 'before': 'On.isBefore',
101
+ }
@@ -0,0 +1,110 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """XPath 查询引擎:在控件树 JSON 上执行 xpath 查询。
4
+
5
+ 设备端 uitest 不支持 xpath,通过 dumpLayout 获取控件树后,
6
+ 在客户端实现 xpath 查询。
7
+
8
+ 支持的 xpath 语法(子集):
9
+ //TagName 查找所有指定类型的后代节点
10
+ //* 查找所有节点
11
+ //TagName[@attr="val"] 属性等于
12
+ //TagName[contains(@attr, "val")] 属性包含
13
+ //TagName[@a="v1" and @b="v2"] 多属性 AND 匹配
14
+
15
+ 控件树节点结构:
16
+ {
17
+ "attributes": {"type": "Text", "text": "设置", ...},
18
+ "children": [...]
19
+ }
20
+ """
21
+ import re
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ # xpath 主模式://Tag[predicate?]
25
+ _XPATH_RE = re.compile(r'^//(\w+|\*)(?:\[(.+)\])?$')
26
+
27
+ # 属性等于:@attr="value"
28
+ _ATTR_EQ_RE = re.compile(r'@(\w+)\s*=\s*"([^"]*)"')
29
+
30
+ # 属性包含:contains(@attr, "value")
31
+ _ATTR_CONTAINS_RE = re.compile(r'contains\s*\(\s*@(\w+)\s*,\s*"([^"]*)"\s*\)')
32
+
33
+
34
+ def parse_xpath(xpath: str) -> Optional[Dict[str, Any]]:
35
+ """解析 xpath 表达式,返回查询计划。
36
+
37
+ Returns:
38
+ {"tag": str, "conditions": [{"attr": str, "op": str, "value": str}]}
39
+ 解析失败返回 None。
40
+ """
41
+ match = _XPATH_RE.match(xpath.strip())
42
+ if not match:
43
+ return None
44
+ tag = match.group(1)
45
+ predicate = match.group(2)
46
+ conditions = []
47
+ if predicate:
48
+ # 按 " and " 分割多条件
49
+ parts = re.split(r'\s+and\s+', predicate)
50
+ for part in parts:
51
+ part = part.strip()
52
+ m = _ATTR_CONTAINS_RE.match(part)
53
+ if m:
54
+ conditions.append({
55
+ "attr": m.group(1), "op": "contains", "value": m.group(2)
56
+ })
57
+ continue
58
+ m = _ATTR_EQ_RE.match(part)
59
+ if m:
60
+ conditions.append({
61
+ "attr": m.group(1), "op": "equals", "value": m.group(2)
62
+ })
63
+ continue
64
+ return None
65
+ return {"tag": tag, "conditions": conditions}
66
+
67
+
68
+ def query_xpath(tree: Dict[str, Any], xpath: str) -> List[Dict[str, Any]]:
69
+ """在控件树上执行 xpath 查询,返回匹配的节点列表。"""
70
+ plan = parse_xpath(xpath)
71
+ if plan is None:
72
+ return []
73
+ results: List[Dict[str, Any]] = []
74
+ _traverse(tree, plan, results)
75
+ return results
76
+
77
+
78
+ def _traverse(node: Dict[str, Any], plan: Dict[str, Any],
79
+ results: List[Dict[str, Any]]) -> None:
80
+ """深度优先遍历控件树,收集匹配节点。"""
81
+ attrs = node.get("attributes") or {}
82
+ tag = plan["tag"]
83
+ node_type = attrs.get("type", "")
84
+
85
+ # tag 匹配(* 匹配任意)
86
+ tag_matched = (tag == "*" or node_type == tag)
87
+ if tag_matched and _match_conditions(attrs, plan["conditions"]):
88
+ results.append(node)
89
+
90
+ for child in node.get("children") or []:
91
+ _traverse(child, plan, results)
92
+
93
+
94
+ def _match_conditions(attrs: Dict[str, Any],
95
+ conditions: List[Dict[str, Any]]) -> bool:
96
+ """检查节点属性是否满足全部条件。"""
97
+ for cond in conditions:
98
+ attr_name = cond["attr"]
99
+ expected = cond["value"]
100
+ actual = attrs.get(attr_name)
101
+ if actual is None:
102
+ return False
103
+ actual_str = str(actual) if not isinstance(actual, bool) else str(actual).lower()
104
+ if cond["op"] == "equals":
105
+ if actual_str != expected:
106
+ return False
107
+ elif cond["op"] == "contains":
108
+ if expected not in actual_str:
109
+ return False
110
+ return True
@@ -0,0 +1,12 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """鸿蒙 RPC 协议与远程对象管理。
4
+
5
+ 子模块:
6
+ - proxy_v2:bin 模式 RPC 传输
7
+ - remote_object:远程对象生命周期管理
8
+ - client:RPC 客户端,设备端 API 调用与返回值处理
9
+
10
+ 仅支持 HarmonyOS 5.0.0+(API 12+),设备端 uitest 服务统一使用
11
+ api9+ 命名(Driver/Component/On),不再兼容 api8 旧命名。
12
+ """