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,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
|
+
"""
|