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,126 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""RpcClient:设备端 API 调用与返回值处理。
|
|
4
|
+
|
|
5
|
+
统一使用 bin 模式(proxy_v2),废弃 hap 模式。
|
|
6
|
+
负责:
|
|
7
|
+
- 构造 RPC 消息(api/this/args/message_type)
|
|
8
|
+
- 调用 proxy_v2 发送
|
|
9
|
+
- 处理返回值(解析 JSON、错误检测、异常分类)
|
|
10
|
+
- 远程对象生命周期管理
|
|
11
|
+
|
|
12
|
+
仅支持 HarmonyOS 5.0.0+(API 12+),设备端 uitest 服务统一使用
|
|
13
|
+
api9+ 命名(Driver/Component/On),不再兼容 api8 旧命名。
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
from typing import Any, Optional, TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from devhelmkit.exceptions import (
|
|
19
|
+
AgentError,
|
|
20
|
+
BackendObjectDroppedError,
|
|
21
|
+
RpcError,
|
|
22
|
+
)
|
|
23
|
+
from devhelmkit.harmony.rpc.proxy_v2 import rpc as rpc_send
|
|
24
|
+
from devhelmkit.harmony.rpc.remote_object import RemoteObjectManager
|
|
25
|
+
from devhelmkit.utils import logger
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from devhelmkit.harmony.device.hdc import HdcDevice
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RpcClient:
|
|
32
|
+
"""RPC 客户端,统一使用 bin 模式。"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, device: 'HdcDevice'):
|
|
35
|
+
self._device = device
|
|
36
|
+
self._remote_objects = RemoteObjectManager()
|
|
37
|
+
|
|
38
|
+
def call(self, api_name: str, this_ref: str, args: list) -> Any:
|
|
39
|
+
"""调用设备端 API。
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_name: API 名称(api9+ 风格,如 "Driver.click"、"On.text")
|
|
43
|
+
this_ref: 对象引用(如 'Driver#0'、'Component#3')
|
|
44
|
+
args: 参数列表(递归处理 FrontEndClass → ref, JsonBase → dict, Enum → value)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
设备端返回结果
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
RpcError: RPC 调用失败
|
|
51
|
+
BackendObjectDroppedError: 后端对象引用已失效
|
|
52
|
+
AgentError: 设备端 Agent 异常
|
|
53
|
+
"""
|
|
54
|
+
msg = {
|
|
55
|
+
'api': api_name,
|
|
56
|
+
'this': this_ref,
|
|
57
|
+
'args': args,
|
|
58
|
+
'message_type': 'hypium'
|
|
59
|
+
}
|
|
60
|
+
logger.debug("RPC -> %s this=%s args=%r", api_name, this_ref, args)
|
|
61
|
+
reply = rpc_send(self._device, msg)
|
|
62
|
+
result = self._handle_reply(reply, api_name)
|
|
63
|
+
logger.debug("RPC <- %s result=%r", api_name, result)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
def _handle_reply(self, reply: str, api_name: str) -> Any:
|
|
67
|
+
"""处理设备端返回值,解析 JSON 并检测错误。"""
|
|
68
|
+
try:
|
|
69
|
+
result = json.loads(reply)
|
|
70
|
+
except json.JSONDecodeError as e:
|
|
71
|
+
logger.error("RPC 返回值解析失败 [%s]: %s", api_name, reply[:200])
|
|
72
|
+
raise RpcError(
|
|
73
|
+
"RPC 返回值解析失败: %s" % e,
|
|
74
|
+
api=api_name, reply=reply
|
|
75
|
+
) from e
|
|
76
|
+
|
|
77
|
+
error = result.get('error')
|
|
78
|
+
if error:
|
|
79
|
+
logger.warn("RPC 调用失败 [%s]: %s", api_name, error)
|
|
80
|
+
return self._raise_for_error(error, api_name, reply)
|
|
81
|
+
|
|
82
|
+
# 设备端异常时可能返回 {"exception":{"code":...,"message":...}}
|
|
83
|
+
exception = result.get('exception')
|
|
84
|
+
if exception:
|
|
85
|
+
msg = exception.get('message', str(exception))
|
|
86
|
+
logger.warn("RPC 调用异常 [%s]: %s", api_name, msg)
|
|
87
|
+
raise RpcError(
|
|
88
|
+
"RPC 调用异常: %s" % msg,
|
|
89
|
+
api=api_name, reply=reply
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return result.get('result')
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _raise_for_error(error: str, api_name: str, reply: str) -> None:
|
|
96
|
+
"""根据错误信息抛出对应异常。"""
|
|
97
|
+
error_lower = str(error).lower()
|
|
98
|
+
|
|
99
|
+
# 后端对象引用失效
|
|
100
|
+
if 'object' in error_lower and ('dropped' in error_lower or
|
|
101
|
+
'released' in error_lower or
|
|
102
|
+
'invalid' in error_lower):
|
|
103
|
+
raise BackendObjectDroppedError(
|
|
104
|
+
"后端对象引用已失效: %s" % error,
|
|
105
|
+
api=api_name, reply=reply
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Agent 进程异常
|
|
109
|
+
if 'agent' in error_lower or 'uitest' in error_lower or \
|
|
110
|
+
'crash' in error_lower:
|
|
111
|
+
raise AgentError("Agent 异常: %s" % error)
|
|
112
|
+
|
|
113
|
+
raise RpcError(
|
|
114
|
+
"RPC 调用失败 [%s]: %s" % (api_name, error),
|
|
115
|
+
api=api_name, reply=reply
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def device(self) -> 'HdcDevice':
|
|
120
|
+
"""底层设备通道,供 Captures/Gestures 等非 callHypiumApi 模块复用。"""
|
|
121
|
+
return self._device
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def remote_objects(self) -> RemoteObjectManager:
|
|
125
|
+
"""远程对象管理器。"""
|
|
126
|
+
return self._remote_objects
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""bin 模式 RPC 传输,保留设备端协议。
|
|
4
|
+
|
|
5
|
+
proxy_v2 不读取 device 内部代理字段,只调用统一传输入口 device.rpc_call()。
|
|
6
|
+
设备端协议消息格式:
|
|
7
|
+
{
|
|
8
|
+
"module": "com.ohos.devicetest.hypiumApiHelper",
|
|
9
|
+
"method": "callHypiumApi",
|
|
10
|
+
"params": <内层消息字典>,
|
|
11
|
+
"request_id": "<时间戳>"
|
|
12
|
+
}
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from devhelmkit.harmony.device.hdc import HdcDevice
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _generate_request_id() -> str:
|
|
23
|
+
"""生成基于时间戳的请求 ID。"""
|
|
24
|
+
return datetime.now().strftime("%Y%m%d%H%M%S%f")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _send(device: 'HdcDevice', method: str, params: dict) -> str:
|
|
28
|
+
"""构造 bin 模式 RPC 消息并发送至设备端。
|
|
29
|
+
|
|
30
|
+
统一负责 request_id 生成、full_msg 组装、JSON 序列化与设备调用,
|
|
31
|
+
对外函数仅构造 params 后委托本函数。
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
device: HdcDevice 实例,提供 rpc_call 传输入口
|
|
35
|
+
method: 设备端协议 method 字段(如 callHypiumApi/Captures/Gestures)
|
|
36
|
+
params: 内层 params 消息字典
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
设备端回显的 JSON 字符串
|
|
40
|
+
"""
|
|
41
|
+
full_msg = {
|
|
42
|
+
"module": "com.ohos.devicetest.hypiumApiHelper",
|
|
43
|
+
"method": method,
|
|
44
|
+
"params": params,
|
|
45
|
+
"request_id": _generate_request_id()
|
|
46
|
+
}
|
|
47
|
+
full_msg_str = json.dumps(full_msg, ensure_ascii=False,
|
|
48
|
+
separators=(',', ':'))
|
|
49
|
+
return device.rpc_call(full_msg_str)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def rpc(device: 'HdcDevice', msg: dict) -> str:
|
|
53
|
+
"""发送 bin 模式 RPC,返回设备端回显字符串。
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
device: HdcDevice 实例,提供 rpc_call 传输入口
|
|
57
|
+
msg: 内层消息字典(含 api/this/args/message_type)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
设备端回显的 JSON 字符串
|
|
61
|
+
"""
|
|
62
|
+
return _send(device, "callHypiumApi", msg)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def rpc_captures(device: 'HdcDevice', api: str, args: dict) -> str:
|
|
66
|
+
"""发送 Captures 模块 RPC(如 captureLayout)。
|
|
67
|
+
|
|
68
|
+
Captures 模块协议与 callHypiumApi 不同:
|
|
69
|
+
- method 为 "Captures"
|
|
70
|
+
- params 中无 this/message_type,args 为对象而非数组
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
device: HdcDevice 实例
|
|
74
|
+
api: API 名称(如 "captureLayout")
|
|
75
|
+
args: 参数对象
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
设备端回显的 JSON 字符串
|
|
79
|
+
"""
|
|
80
|
+
params = {
|
|
81
|
+
"api": api,
|
|
82
|
+
"args": args
|
|
83
|
+
}
|
|
84
|
+
return _send(device, "Captures", params)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def rpc_gestures(device: 'HdcDevice', api: str, args: dict) -> str:
|
|
88
|
+
"""发送 Gestures 模块 RPC(如 touchDown/touchMove/touchUp)。
|
|
89
|
+
|
|
90
|
+
Gestures 模块协议与 callHypiumApi 不同:
|
|
91
|
+
- method 为 "Gestures"
|
|
92
|
+
- params 中无 this/message_type,args 为对象(如 {x, y})而非数组
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
device: HdcDevice 实例
|
|
96
|
+
api: API 名称(如 "touchDown")
|
|
97
|
+
args: 参数对象
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
设备端回显的 JSON 字符串
|
|
101
|
+
"""
|
|
102
|
+
params = {
|
|
103
|
+
"api": api,
|
|
104
|
+
"args": args
|
|
105
|
+
}
|
|
106
|
+
return _send(device, "Gestures", params)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""远程对象生命周期管理。
|
|
4
|
+
|
|
5
|
+
跟踪设备端远程对象引用(如 Driver#0、Component#3、On#seed)的创建与回收。
|
|
6
|
+
活跃对象使用 WeakSet 跟踪,待回收对象累积超过 batch_size 时触发批量清理。
|
|
7
|
+
"""
|
|
8
|
+
from typing import Any, Set
|
|
9
|
+
from weakref import WeakSet
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_BATCH_SIZE = 20
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RemoteObjectManager:
|
|
16
|
+
"""管理设备端远程对象引用的生命周期。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self.total_registered_objects = 0
|
|
20
|
+
self.total_removed_objects = 0
|
|
21
|
+
self.batch_size = DEFAULT_BATCH_SIZE
|
|
22
|
+
# 待回收的对象引用集合
|
|
23
|
+
self.pending_release_refs: Set[str] = set()
|
|
24
|
+
# 活跃对象弱引用集合(不阻止 GC)
|
|
25
|
+
self.active_objects = WeakSet()
|
|
26
|
+
# 标记是否已执行 release_all,禁止后续 add_object
|
|
27
|
+
self._released = False
|
|
28
|
+
|
|
29
|
+
def add_object(self, obj: Any) -> None:
|
|
30
|
+
"""注册远程对象到活跃集合。"""
|
|
31
|
+
if self._released:
|
|
32
|
+
return
|
|
33
|
+
remains = len(self.active_objects)
|
|
34
|
+
self.active_objects.add(obj)
|
|
35
|
+
if len(self.active_objects) > remains:
|
|
36
|
+
self.total_registered_objects += 1
|
|
37
|
+
|
|
38
|
+
def remove_object(self, backend_obj_ref: str) -> None:
|
|
39
|
+
"""标记远程对象引用为待回收。"""
|
|
40
|
+
self.total_removed_objects += 1
|
|
41
|
+
self.pending_release_refs.add(backend_obj_ref)
|
|
42
|
+
|
|
43
|
+
def release_all(self) -> None:
|
|
44
|
+
"""释放所有远程对象。"""
|
|
45
|
+
self._released = True
|
|
46
|
+
for item in self.active_objects:
|
|
47
|
+
if hasattr(item, 'release'):
|
|
48
|
+
item.release()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""UiObject:HarmonyOS 平台控件对象。
|
|
4
|
+
|
|
5
|
+
UiObject 只持有 SelectorSpec 与驱动引用,不直接拼底层 RPC 消息。
|
|
6
|
+
控件查找、选择器转换、远程对象引用和弹窗处理统一交给 HarmonyDriver /
|
|
7
|
+
ComponentFinder。每次操作都重新定位控件,对齐 U2 单对象模型。
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Optional, Tuple, Union, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from devhelmkit.core.base_component import BaseComponent
|
|
14
|
+
from devhelmkit.core.selector_spec import SelectorSpec, build_selector
|
|
15
|
+
from devhelmkit.model.rect import Rect
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from PIL.Image import Image
|
|
19
|
+
from devhelmkit.harmony.driver import HarmonyDriver
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UiObject(BaseComponent):
|
|
23
|
+
"""HarmonyOS 平台控件对象。
|
|
24
|
+
|
|
25
|
+
所有操作通过 HarmonyDriver 门面委托给 ComponentFinder,
|
|
26
|
+
由 ComponentFinder 负责 By/On 链构造、findComponent 和方法调用。
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# ============================================================
|
|
30
|
+
# 内部:控件操作门面
|
|
31
|
+
# ============================================================
|
|
32
|
+
|
|
33
|
+
def _call_component(self, method: str,
|
|
34
|
+
args: Optional[list] = None) -> Any:
|
|
35
|
+
"""通过驱动门面执行控件操作。"""
|
|
36
|
+
return self._driver.call_component(self._selector, method, args or [])
|
|
37
|
+
|
|
38
|
+
# ============================================================
|
|
39
|
+
# 点击类
|
|
40
|
+
# ============================================================
|
|
41
|
+
|
|
42
|
+
def click(self) -> None:
|
|
43
|
+
self._call_component("click")
|
|
44
|
+
|
|
45
|
+
def long_click(self, duration: float = 0.5) -> None:
|
|
46
|
+
self._call_component("longClick", [duration])
|
|
47
|
+
|
|
48
|
+
def double_click(self) -> None:
|
|
49
|
+
self._call_component("doubleClick")
|
|
50
|
+
|
|
51
|
+
def click_exists(self, timeout: float = 0) -> bool:
|
|
52
|
+
return self._call_component("clickIfExists", [timeout])
|
|
53
|
+
|
|
54
|
+
# ============================================================
|
|
55
|
+
# 文本类
|
|
56
|
+
# ============================================================
|
|
57
|
+
|
|
58
|
+
def set_text(self, text: str) -> None:
|
|
59
|
+
self._call_component("setText", [text])
|
|
60
|
+
|
|
61
|
+
def get_text(self) -> str:
|
|
62
|
+
return self._call_component("getText")
|
|
63
|
+
|
|
64
|
+
def clear_text(self) -> None:
|
|
65
|
+
self._call_component("clearText")
|
|
66
|
+
|
|
67
|
+
def input_text(self, text: str) -> None:
|
|
68
|
+
self.set_text(text)
|
|
69
|
+
|
|
70
|
+
# ============================================================
|
|
71
|
+
# 状态类
|
|
72
|
+
# ============================================================
|
|
73
|
+
|
|
74
|
+
def exists(self) -> bool:
|
|
75
|
+
return self._driver.component_exists(self._selector)
|
|
76
|
+
|
|
77
|
+
def wait(self, timeout: float) -> bool:
|
|
78
|
+
return self._driver.wait_component(self._selector, timeout)
|
|
79
|
+
|
|
80
|
+
def wait_gone(self, timeout: float) -> bool:
|
|
81
|
+
return self._driver.wait_component_gone(self._selector, timeout)
|
|
82
|
+
|
|
83
|
+
# ============================================================
|
|
84
|
+
# 信息类
|
|
85
|
+
# ============================================================
|
|
86
|
+
|
|
87
|
+
def _collect_properties(self) -> dict:
|
|
88
|
+
"""收集控件共有属性(文本、类型、布尔状态)。
|
|
89
|
+
|
|
90
|
+
info 与 get_all_properties 共享此基础集合,
|
|
91
|
+
info 在此基础上补充 description 与 bounds。
|
|
92
|
+
"""
|
|
93
|
+
return {
|
|
94
|
+
"text": self._call_component("getText"),
|
|
95
|
+
"id": self._call_component("getId"),
|
|
96
|
+
"type": self._call_component("getType"),
|
|
97
|
+
"enabled": self._call_component("isEnabled"),
|
|
98
|
+
"focused": self._call_component("isFocused"),
|
|
99
|
+
"selected": self._call_component("isSelected"),
|
|
100
|
+
"clickable": self._call_component("isClickable"),
|
|
101
|
+
"long_clickable": self._call_component("isLongClickable"),
|
|
102
|
+
"scrollable": self._call_component("isScrollable"),
|
|
103
|
+
"checkable": self._call_component("isCheckable"),
|
|
104
|
+
"checked": self._call_component("isChecked"),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def info(self) -> dict:
|
|
109
|
+
"""控件综合信息(文本、类型、状态、边界等)。
|
|
110
|
+
|
|
111
|
+
设备端无 getInfo API,逐项调用属性方法组装。
|
|
112
|
+
"""
|
|
113
|
+
props = self._collect_properties()
|
|
114
|
+
props["description"] = self._call_component("getDescription")
|
|
115
|
+
props["bounds"] = self._call_component("getBounds")
|
|
116
|
+
return props
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def bounds(self) -> Rect:
|
|
120
|
+
result = self._call_component("getBounds")
|
|
121
|
+
return _to_rect(result)
|
|
122
|
+
|
|
123
|
+
def center(self) -> Tuple[int, int]:
|
|
124
|
+
return self.bounds.center.as_tuple()
|
|
125
|
+
|
|
126
|
+
def get_attribute(self, name: str) -> Any:
|
|
127
|
+
return self._call_component("getAttribute", [name])
|
|
128
|
+
|
|
129
|
+
def screenshot(self, filename: Optional[str] = None) -> Union['Image', str, None]:
|
|
130
|
+
return self._driver.screenshot(filename=filename, area=self._selector)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def description(self) -> str:
|
|
134
|
+
return self._call_component("getDescription")
|
|
135
|
+
|
|
136
|
+
def get_hint(self) -> str:
|
|
137
|
+
return self._call_component("getHint")
|
|
138
|
+
|
|
139
|
+
def get_all_properties(self) -> dict:
|
|
140
|
+
"""获取控件全部布尔属性与文本。"""
|
|
141
|
+
return self._collect_properties()
|
|
142
|
+
|
|
143
|
+
def get_original_text(self) -> str:
|
|
144
|
+
return self._call_component("getOriginalText")
|
|
145
|
+
|
|
146
|
+
# ============================================================
|
|
147
|
+
# 布尔属性
|
|
148
|
+
# ============================================================
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def is_long_clickable(self) -> bool:
|
|
152
|
+
return self._call_component("isLongClickable")
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def is_checked(self) -> bool:
|
|
156
|
+
return self._call_component("isChecked")
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def is_checkable(self) -> bool:
|
|
160
|
+
return self._call_component("isCheckable")
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def is_selected(self) -> bool:
|
|
164
|
+
return self._call_component("isSelected")
|
|
165
|
+
|
|
166
|
+
# ============================================================
|
|
167
|
+
# 拖拽类
|
|
168
|
+
# ============================================================
|
|
169
|
+
|
|
170
|
+
def drag_to(self, x: int, y: int) -> None:
|
|
171
|
+
self._call_component("dragTo", [x, y])
|
|
172
|
+
|
|
173
|
+
def drag_to_component(self, other: 'UiObject') -> None:
|
|
174
|
+
self._call_component("dragToComponent", [other._selector])
|
|
175
|
+
|
|
176
|
+
# ============================================================
|
|
177
|
+
# 缩放类
|
|
178
|
+
# ============================================================
|
|
179
|
+
|
|
180
|
+
def pinch_in(self, scale: float = 0.5) -> None:
|
|
181
|
+
self._call_component("pinchIn", [scale])
|
|
182
|
+
|
|
183
|
+
def pinch_out(self, scale: float = 1.5) -> None:
|
|
184
|
+
self._call_component("pinchOut", [scale])
|
|
185
|
+
|
|
186
|
+
# ============================================================
|
|
187
|
+
# 滚动类
|
|
188
|
+
# ============================================================
|
|
189
|
+
|
|
190
|
+
def scroll_search(self, target, vertical: bool = True,
|
|
191
|
+
offset: Optional[int] = None) -> Optional['UiObject']:
|
|
192
|
+
return self._driver.scroll_search(
|
|
193
|
+
self._selector, target, vertical, offset
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def scroll_to_top(self, speed: int = 600) -> None:
|
|
197
|
+
self._call_component("scrollToTop", [speed])
|
|
198
|
+
|
|
199
|
+
def scroll_to_bottom(self, speed: int = 600) -> None:
|
|
200
|
+
self._call_component("scrollToBottom", [speed])
|
|
201
|
+
|
|
202
|
+
# ============================================================
|
|
203
|
+
# 关系选择器:返回新的 UiObject,持有衍生 SelectorSpec
|
|
204
|
+
# ============================================================
|
|
205
|
+
|
|
206
|
+
def child(self, **kwargs) -> 'UiObject':
|
|
207
|
+
child_spec = build_selector(**kwargs)
|
|
208
|
+
new_spec = SelectorSpec.with_relation(self._selector, 'child', child_spec)
|
|
209
|
+
return UiObject(self._driver, new_spec)
|
|
210
|
+
|
|
211
|
+
def sibling(self, **kwargs) -> 'UiObject':
|
|
212
|
+
sibling_spec = build_selector(**kwargs)
|
|
213
|
+
new_spec = SelectorSpec.with_relation(
|
|
214
|
+
self._selector, 'sibling', sibling_spec
|
|
215
|
+
)
|
|
216
|
+
return UiObject(self._driver, new_spec)
|
|
217
|
+
|
|
218
|
+
def after(self, **kwargs) -> 'UiObject':
|
|
219
|
+
after_spec = build_selector(**kwargs)
|
|
220
|
+
new_spec = SelectorSpec.with_relation(self._selector, 'after', after_spec)
|
|
221
|
+
return UiObject(self._driver, new_spec)
|
|
222
|
+
|
|
223
|
+
def before(self, **kwargs) -> 'UiObject':
|
|
224
|
+
before_spec = build_selector(**kwargs)
|
|
225
|
+
new_spec = SelectorSpec.with_relation(self._selector, 'before', before_spec)
|
|
226
|
+
return UiObject(self._driver, new_spec)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _to_rect(data: Any) -> Rect:
|
|
230
|
+
"""将设备端返回的坐标数据转为 Rect。
|
|
231
|
+
|
|
232
|
+
设备端 getBounds 可能返回字典或列表,统一转换为 Rect。
|
|
233
|
+
"""
|
|
234
|
+
if isinstance(data, dict):
|
|
235
|
+
return Rect(
|
|
236
|
+
left=int(data.get('left', 0)),
|
|
237
|
+
top=int(data.get('top', 0)),
|
|
238
|
+
right=int(data.get('right', 0)),
|
|
239
|
+
bottom=int(data.get('bottom', 0)),
|
|
240
|
+
)
|
|
241
|
+
if isinstance(data, (list, tuple)) and len(data) >= 4:
|
|
242
|
+
return Rect(
|
|
243
|
+
left=int(data[0]), top=int(data[1]),
|
|
244
|
+
right=int(data[2]), bottom=int(data[3]),
|
|
245
|
+
)
|
|
246
|
+
return Rect(left=0, top=0, right=0, bottom=0)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""UiWindow:HarmonyOS 平台窗口对象。
|
|
4
|
+
|
|
5
|
+
窗口操作能力较控件操作少,当前阶段保留基础结构,
|
|
6
|
+
后续按需补充基于 Driver.findWindow 的实现。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from devhelmkit.core.base_window import BaseWindow
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from devhelmkit.harmony.driver import HarmonyDriver
|
|
16
|
+
from devhelmkit.harmony.rpc.client import RpcClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UiWindow(BaseWindow):
|
|
20
|
+
"""HarmonyOS 平台窗口对象。
|
|
21
|
+
|
|
22
|
+
持有驱动引用,通过 RPC 调用设备端窗口 API。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, driver: 'HarmonyDriver',
|
|
26
|
+
rpc: 'RpcClient', window_id: Optional[int] = None):
|
|
27
|
+
self._driver = driver
|
|
28
|
+
self._rpc = rpc
|
|
29
|
+
self._window_id = window_id
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def size(self) -> Tuple[int, int]:
|
|
33
|
+
"""窗口大小,设备端暂无窗口级尺寸 API,回退为屏幕尺寸。"""
|
|
34
|
+
return self._driver.get_display_size()
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def info(self) -> Dict[str, Any]:
|
|
38
|
+
"""窗口信息,设备端暂无窗口信息 API,返回空字典占位。"""
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
def get_windows(self) -> List['UiWindow']:
|
|
42
|
+
"""获取所有窗口,设备端暂无枚举窗口 API,返回空列表占位。"""
|
|
43
|
+
return []
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""webview 自动化子模块。
|
|
4
|
+
|
|
5
|
+
提供 webview 页面级自动化能力,基于 selenium webdriver。
|
|
6
|
+
|
|
7
|
+
使用方式:
|
|
8
|
+
from devhelmkit.harmony.webview import WebViewDriver
|
|
9
|
+
|
|
10
|
+
wv = WebViewDriver(device)
|
|
11
|
+
wv.connect("com.huawei.hmos.browser")
|
|
12
|
+
wv.driver.get("https://www.baidu.com")
|
|
13
|
+
wv.close()
|
|
14
|
+
"""
|
|
15
|
+
from devhelmkit.harmony.webview.webview_driver import WebViewDriver
|
|
16
|
+
|
|
17
|
+
__all__ = ["WebViewDriver"]
|