Skip to content

Commit a2e7910

Browse files
committed
feat: add dump_hierarchy and ToastWatcher
1 parent 37e1304 commit a2e7910

File tree

9 files changed

+183
-48
lines changed

9 files changed

+183
-48
lines changed

.flake8

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ ignore =
55
F401
66
E402
77
W292
8-
F403
8+
F403
9+
F821

.github/workflows/release.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
name: Release
22

3+
# on:
4+
# release:
5+
# types: [created]
36
on:
4-
release:
5-
types: [created]
7+
push:
8+
tags:
9+
- '*.*.*'
610

711
jobs:
812
build-n-publish:

README.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ d.screenshot(lpath)
6363

6464
# Device touch
6565
d.click(500, 1000)
66-
d.click(0.5, 0.4)
66+
d.click(0.5, 0.4) # "If of type float, it represents percentage coordinates."
6767
d.double_click(500, 1000)
6868
d.double_click(0.5, 0.4)
6969
d.long_click(500, 1000)
@@ -100,21 +100,28 @@ d(text="showToast").info
100100
# }
101101

102102
d(id="swiper").exists()
103-
d(type="Button", text="showToast").exists()
104-
d(text="showToast", isAfter=True).exists()
105-
d(text="showToast").click_if_exists()
103+
d(type="Button", text="tab_recrod").exists()
104+
d(text="tab_recrod", isAfter=True).exists()
105+
d(text="tab_recrod").click_if_exists()
106106
d(type="Button", index=3).click()
107-
d(text="showToast").double_click()
108-
d(text="showToast").long_click()
107+
d(text="tab_recrod").double_click()
108+
d(text="tab_recrod").long_click()
109109

110110
component: ComponentData = d(type="ListItem", index=1).find_component()
111111
d(type="ListItem").drag_to(component)
112112

113-
d(text="showToast").input_text("abc")
114-
d(text="showToast").clear_text()
115-
d(text="showToast").pinch_in()
116-
d(text="showToast").pinch_out()
113+
d(text="tab_recrod").input_text("abc")
114+
d(text="tab_recrod").clear_text()
115+
d(text="tab_recrod").pinch_in()
116+
d(text="tab_recrod").pinch_out()
117117

118+
# Dump hierarchy
119+
d.dump_hierarchy()
120+
121+
# Toast Watcher
122+
d.toast_watcher.start()
123+
d(type="Button", text="tab_recrod").click() # 触发toast的操作
124+
toast = d.toast_watcher.get()
118125

119126
```
120127

docs/DEVELOP.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,27 @@
168168
{"result":"UiWindow#10"}
169169
```
170170

171+
### uiEventObserverOnce
172+
**send**
173+
```
174+
{"module":"com.ohos.devicetest.hypiumApiHelper","method":"callHypiumApi","params":{"api":"Driver.uiEventObserverOnce","this":"Driver#0","args":["toastShow"],"message_type":"hypium"},"request_id":"20240905144543056211","client":"127.0.0.1"}
175+
```
176+
**recv**
177+
```
178+
{"result":true}
179+
```
180+
181+
182+
### getRecentUiEvent
183+
**send**
184+
```
185+
{"module":"com.ohos.devicetest.hypiumApiHelper","method":"callHypiumApi","params":{"api":"Driver.getRecentUiEvent","this":"Driver#0","args":[3000],"message_type":"hypium"},"request_id":"20240905143857794307","client":"127.0.0.1"}
186+
```
187+
**recv**
188+
```
189+
{"result":{"bundleName":"com.samples.test.uitest","text":"testMessage","type":"Toast"}}
190+
```
191+
171192

172193
## Component
173194
### Component.getId

hmdriver2/_client.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ def _send_msg(self, msg: typing.Dict):
6464
}
6565
"""
6666
msg = json.dumps(msg, ensure_ascii=False, separators=(',', ':'))
67-
logger.debug(f"send msg: {msg}")
67+
logger.debug(f"sendMsg: {msg}")
6868
self.sock.sendall(msg.encode('utf-8') + b'\n')
6969

7070
def _recv_msg(self, buff_size: int = 1024, decode=False) -> typing.Union[bytearray, str]:
7171
try:
7272
relay = self.sock.recv(buff_size)
7373
if decode:
7474
relay = relay.decode()
75-
logger.debug(f"recv msg: {relay}")
75+
logger.debug(f"recvMsg: {relay}")
7676
return relay
7777
except (socket.timeout, UnicodeDecodeError) as e:
7878
logger.warning(e)
@@ -100,26 +100,13 @@ def invoke(self, api: str, this: str = "Driver#0", args: typing.List = []) -> Hy
100100
return HypiumResponse(**(json.loads(result)))
101101

102102
def start(self):
103-
"""
104-
105-
Args:
106-
file_path (str): Path where the recorded video will be saved.
107-
108-
Raises:
109-
RuntimeError: If the screen capture fails to start.
110-
"""
111103
logger.info("Start client connection")
112104
self._init_so_resource()
113105
self._restart_uitest_service()
114106

115107
self._connect_sock()
116108

117109
def release(self):
118-
"""
119-
120-
Raises:
121-
RuntimeError: If the screen capture fails to start.
122-
"""
123110
logger.info("Release client connection")
124111
try:
125112
if self.sock:
@@ -176,6 +163,7 @@ def __get_local_md5sum(f: str) -> str:
176163
def _restart_uitest_service(self):
177164
"""
178165
Restart the UITest daemon.
166+
179167
Note: 'hdc shell aa test' will also start a uitest daemon.
180168
$ hdc shell ps -ef |grep uitest
181169
shell 44306 1 25 11:03:37 ? 00:00:16 uitest start-daemon singleness

hmdriver2/_toast.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from .proto import HypiumResponse
4+
5+
6+
class ToastWatcher:
7+
def __init__(self, session: "Driver"): # type: ignore
8+
self.session = session
9+
10+
def start(self) -> bool:
11+
"""
12+
Initiates the observer to listen for a UI toast event
13+
14+
Returns:
15+
bool: True if the observer starts successfully, else False.
16+
"""
17+
api = "Driver.uiEventObserverOnce"
18+
resp: HypiumResponse = self.session._invoke(api, args=["toastShow"])
19+
return resp.result
20+
21+
def get(self, timeout: int = 5) -> str:
22+
"""
23+
Read the latest toast message content from the recent period.
24+
25+
Args:
26+
timeout (int): The maximum time to wait for a toast to appear if there are no matching toasts within the given time.
27+
28+
Returns:
29+
str: The content of the latest toast message.
30+
"""
31+
api = "Driver.getRecentUiEvent"
32+
resp: HypiumResponse = self.session._invoke(api, args=[timeout])
33+
if resp.result:
34+
return resp.result.get("text")
35+
return None

hmdriver2/driver.py

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# -*- coding: utf-8 -*-
22

3-
import atexit
43
import time
54
import uuid
65
from typing import Type, Any, Tuple, Dict, Union, List
@@ -11,11 +10,11 @@
1110
except ImportError:
1211
from cached_property import cached_property
1312

14-
from . import logger
1513
from .exception import DeviceNotFoundError
1614
from ._client import HMClient
1715
from ._uiobject import UiObject
1816
from .hdc import list_devices
17+
from ._toast import ToastWatcher
1918
from .proto import HypiumResponse, KeyCode, Point, DisplayRotation, DeviceInfo
2019

2120

@@ -24,25 +23,30 @@ class Driver:
2423

2524
def __init__(self, serial: str):
2625
self.serial = serial
27-
if not self._check_serial():
26+
if not self._is_device_online():
2827
raise DeviceNotFoundError(f"Device [{self.serial}] not found")
2928

3029
self._client = HMClient(self.serial)
3130
self._this_driver = self._client._hdriver.value # "Driver#0"
3231
self.hdc = self._client.hdc
3332

3433
def __new__(cls: Type[Any], serial: str) -> Any:
34+
"""
35+
Ensure that only one instance of Driver exists per device serial number.
36+
"""
3537
if serial not in cls._instance:
3638
cls._instance[serial] = super().__new__(cls)
3739
return cls._instance[serial]
3840

3941
def __call__(self, **kwargs) -> UiObject:
42+
4043
return UiObject(self._client, **kwargs)
4144

4245
def __del__(self):
43-
self._client.release()
46+
if hasattr(self, '_client') and self._client:
47+
self._client.release()
4448

45-
def _check_serial(self):
49+
def _is_device_online(self):
4650
_serials = list_devices()
4751
return True if self.serial in _serials else False
4852

@@ -59,6 +63,9 @@ def stop_app(self, package_name: str):
5963
self.hdc.stop_app(package_name)
6064

6165
def clear_app(self, package_name: str):
66+
"""
67+
Clear the application's cache and data.
68+
"""
6269
self.hdc.shell(f"bm clean -n {package_name} -c") # clear cache
6370
self.hdc.shell(f"bm clean -n {package_name} -d") # clear data
6471

@@ -96,21 +103,30 @@ def unlock(self):
96103
self.hdc.swipe(0.5 * w, 0.8 * h, 0.5 * w, 0.2 * h)
97104
time.sleep(.5)
98105

106+
def _invoke(self, api: str, args: List = []) -> HypiumResponse:
107+
return self._client.invoke(api, this=self._this_driver, args=args)
108+
99109
@cached_property
100110
def display_size(self) -> Tuple[int, int]:
101111
api = "Driver.getDisplaySize"
102-
resp: HypiumResponse = self._client.invoke(api, self._this_driver)
112+
resp: HypiumResponse = self._invoke(api)
103113
w, h = resp.result.get("x"), resp.result.get("y")
104114
return w, h
105115

106116
@cached_property
107117
def display_rotation(self) -> DisplayRotation:
108118
api = "Driver.getDisplayRotation"
109-
value = self._client.invoke(api, self._this_driver).result
119+
value = self._invoke(api).result
110120
return DisplayRotation.from_value(value)
111121

112122
@cached_property
113123
def device_info(self) -> DeviceInfo:
124+
"""
125+
Get detailed information about the device.
126+
127+
Returns:
128+
DeviceInfo: An object containing various properties of the device.
129+
"""
114130
hdc = self.hdc
115131
return DeviceInfo(
116132
productName=hdc.product_name(),
@@ -123,16 +139,43 @@ def device_info(self) -> DeviceInfo:
123139
displayRotation=self.display_rotation
124140
)
125141

142+
@cached_property
143+
def toast_watcher(self):
144+
return ToastWatcher(self)
145+
126146
def open_url(self, url: str):
127147
self.hdc.shell(f"aa start -U {url}")
128148

129149
def pull_file(self, rpath: str, lpath: str):
150+
"""
151+
Pull a file from the device to the local machine.
152+
153+
Args:
154+
rpath (str): The remote path of the file on the device.
155+
lpath (str): The local path where the file should be saved.
156+
"""
130157
self.hdc.recv_file(rpath, lpath)
131158

132159
def push_file(self, lpath: str, rpath: str):
160+
"""
161+
Push a file from the local machine to the device.
162+
163+
Args:
164+
lpath (str): The local path of the file.
165+
rpath (str): The remote path where the file should be saved on the device.
166+
"""
133167
self.hdc.send_file(lpath, rpath)
134168

135169
def screenshot(self, path: str) -> str:
170+
"""
171+
Take a screenshot of the device display.
172+
173+
Args:
174+
path (str): The local path to save the screenshot.
175+
176+
Returns:
177+
str: The path where the screenshot is saved.
178+
"""
136179
_uuid = uuid.uuid4().hex
137180
_tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg"
138181
self.shell(f"snapshot_display -f {_tmp_path}")
@@ -145,7 +188,14 @@ def shell(self, cmd):
145188

146189
def _to_abs_pos(self, x: Union[int, float], y: Union[int, float]) -> Point:
147190
"""
148-
returns a function which can convert percent size to abs size
191+
Convert percentages to absolute screen coordinates.
192+
193+
Args:
194+
x (Union[int, float]): X coordinate as a percentage or absolute value.
195+
y (Union[int, float]): Y coordinate as a percentage or absolute value.
196+
197+
Returns:
198+
Point: A Point object with absolute screen coordinates.
149199
"""
150200
assert x >= 0
151201
assert y >= 0
@@ -163,21 +213,28 @@ def click(self, x: Union[int, float], y: Union[int, float]):
163213
# self.hdc.tap(point.x, point.y)
164214
point = self._to_abs_pos(x, y)
165215
api = "Driver.click"
166-
self._client.invoke(api, self._this_driver, args=[point.x, point.y])
216+
self._invoke(api, args=[point.x, point.y])
167217

168218
def double_click(self, x: Union[int, float], y: Union[int, float]):
169219
point = self._to_abs_pos(x, y)
170220
api = "Driver.doubleClick"
171-
self._client.invoke(api, self._this_driver, args=[point.x, point.y])
221+
self._invoke(api, args=[point.x, point.y])
172222

173223
def long_click(self, x: Union[int, float], y: Union[int, float]):
174224
point = self._to_abs_pos(x, y)
175225
api = "Driver.longClick"
176-
self._client.invoke(api, self._this_driver, args=[point.x, point.y])
226+
self._invoke(api, args=[point.x, point.y])
177227

178228
def swipe(self, x1, y1, x2, y2, speed=1000):
179229
"""
180-
speed为滑动速率, 范围:200-40000, 不在范围内设为默认值为600, 单位: 像素点/秒
230+
Perform a swipe action on the device screen.
231+
232+
Args:
233+
x1 (float): The start X coordinate as a percentage or absolute value.
234+
y1 (float): The start Y coordinate as a percentage or absolute value.
235+
x2 (float): The end X coordinate as a percentage or absolute value.
236+
y2 (float): The end Y coordinate as a percentage or absolute value.
237+
speed (int, optional): The swipe speed in pixels per second. Default is 1000. Range: 200-40000. If not within the range, set to default value of 600.
181238
"""
182239
point1 = self._to_abs_pos(x1, y1)
183240
point2 = self._to_abs_pos(x2, y2)
@@ -187,3 +244,12 @@ def swipe(self, x1, y1, x2, y2, speed=1000):
187244
def input_text(self, x, y, text: str):
188245
point = self._to_abs_pos(x, y)
189246
self.hdc.input_text(point.x, point.y, text)
247+
248+
def dump_hierarchy(self) -> Dict:
249+
"""
250+
Dump the UI hierarchy of the device screen.
251+
252+
Returns:
253+
Dict: The dumped UI hierarchy as a dictionary.
254+
"""
255+
return self.hdc.dump_hierarchy()

0 commit comments

Comments
 (0)