uiautodev 0.11.0__tar.gz → 0.11.1__tar.gz
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.
Potentially problematic release.
This version of uiautodev might be problematic. Click here for more details.
- {uiautodev-0.11.0 → uiautodev-0.11.1}/PKG-INFO +4 -2
- {uiautodev-0.11.0 → uiautodev-0.11.1}/pyproject.toml +1 -1
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/__init__.py +1 -1
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/harmony.py +149 -9
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/model.py +1 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/provider.py +10 -4
- {uiautodev-0.11.0 → uiautodev-0.11.1}/LICENSE +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/README.md +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/__main__.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/app.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/appium_proxy.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/binaries/scrcpy_server.jar +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/case.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/cli.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/command_proxy.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/command_types.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/common.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/android.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/appium.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/base_driver.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/ios.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/mock.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/testdata/layout.json +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/udt/udt.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/exceptions.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/remote/android_input.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/remote/harmony_mjpeg.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/remote/keycode.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/remote/scrcpy.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/remote/touch_controller.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/router/android.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/router/device.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/router/proxy.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/router/xml.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/static/demo.html +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/utils/common.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/utils/envutils.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/utils/exceptions.py +0 -0
- {uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/utils/usbmux.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: uiautodev
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: codeskyblue
|
|
7
8
|
Author-email: codeskyblue@gmail.com
|
|
8
9
|
Requires-Python: >=3.8,<4.0
|
|
@@ -14,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
19
|
Requires-Dist: Pillow
|
|
18
20
|
Requires-Dist: adbutils (>=2.8.10,<3)
|
|
19
21
|
Requires-Dist: click (>=8.1.7,<9.0.0)
|
|
@@ -10,7 +10,7 @@ import tempfile
|
|
|
10
10
|
import time
|
|
11
11
|
import uuid
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from typing import List, Optional, Tuple, Union, final
|
|
13
|
+
from typing import List, Optional, Tuple, Union, final, Dict
|
|
14
14
|
|
|
15
15
|
from PIL import Image
|
|
16
16
|
|
|
@@ -31,6 +31,7 @@ def run_command(command: str, timeout: int = 60) -> str:
|
|
|
31
31
|
capture_output=True,
|
|
32
32
|
timeout=timeout,
|
|
33
33
|
text=True,
|
|
34
|
+
errors='ignore',
|
|
34
35
|
input='' # this avoid stdout: "FreeChannelContinue handle->data is nullptr"
|
|
35
36
|
)
|
|
36
37
|
# the hdc shell stderr is (不仅没啥用,还没办法去掉)
|
|
@@ -50,10 +51,10 @@ class HDC:
|
|
|
50
51
|
def __init__(self):
|
|
51
52
|
self.hdc = 'hdc'
|
|
52
53
|
self.tmpdir = tempfile.TemporaryDirectory()
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
def __del__(self):
|
|
55
56
|
self.tmpdir.cleanup()
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
def list_device(self) -> List[str]:
|
|
58
59
|
command = f"{self.hdc} list targets"
|
|
59
60
|
result = run_command(command)
|
|
@@ -65,15 +66,48 @@ class HDC:
|
|
|
65
66
|
return devices
|
|
66
67
|
else:
|
|
67
68
|
return []
|
|
68
|
-
|
|
69
|
+
|
|
69
70
|
def shell(self, serial: str, command: str) -> str:
|
|
70
71
|
command = f"{self.hdc} -t {serial} shell \"{command}\""
|
|
71
72
|
result = run_command(command)
|
|
72
73
|
return result.strip()
|
|
73
|
-
|
|
74
|
+
|
|
75
|
+
def __split_text(self, text: str) -> str:
|
|
76
|
+
return text.split("\n")[0].strip() if text else ""
|
|
77
|
+
|
|
74
78
|
def get_model(self, serial: str) -> str:
|
|
75
79
|
return self.shell(serial, "param get const.product.model")
|
|
76
|
-
|
|
80
|
+
|
|
81
|
+
def get_name(self, serial: str) -> str:
|
|
82
|
+
data = self.shell(serial, "param get const.product.name")
|
|
83
|
+
return self.__split_text(data)
|
|
84
|
+
|
|
85
|
+
def wlan_ip(self, serial: str) -> str:
|
|
86
|
+
data = self.shell(serial, "ifconfig")
|
|
87
|
+
if not data or "not found" in data.lower() or "error" in data.lower():
|
|
88
|
+
logger.warning(f"ifconfig command failed or returned error for serial {serial}: {data!r}")
|
|
89
|
+
return ""
|
|
90
|
+
# Try multiple patterns for IP address
|
|
91
|
+
matches = re.findall(r'inet addr:(?!127)(\d+\.\d+\.\d+\.\d+)', data)
|
|
92
|
+
if not matches:
|
|
93
|
+
matches = re.findall(r'inet (?!127)(\d+\.\d+\.\d+\.\d+)', data)
|
|
94
|
+
if matches:
|
|
95
|
+
return matches[0]
|
|
96
|
+
logger.warning(f"No valid IP address found in ifconfig output for serial {serial}: {data!r}")
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
def sdk_version(self, serial: str) -> str:
|
|
100
|
+
data = self.shell(serial, "param get const.ohos.apiversion")
|
|
101
|
+
return self.__split_text(data)
|
|
102
|
+
|
|
103
|
+
def sys_version(self, serial: str) -> str:
|
|
104
|
+
data = self.shell(serial, "param get const.product.software.version")
|
|
105
|
+
return self.__split_text(data)
|
|
106
|
+
|
|
107
|
+
def brand(self, serial: str) -> str:
|
|
108
|
+
data = self.shell(serial, "param get const.product.brand")
|
|
109
|
+
return self.__split_text(data)
|
|
110
|
+
|
|
77
111
|
def pull(self, serial: str, remote: StrOrPath, local: StrOrPath):
|
|
78
112
|
if isinstance(remote, Path):
|
|
79
113
|
remote = remote.as_posix()
|
|
@@ -81,13 +115,13 @@ class HDC:
|
|
|
81
115
|
output = run_command(command)
|
|
82
116
|
if not os.path.exists(local):
|
|
83
117
|
raise HDCError(f"device file: {remote} not found", output)
|
|
84
|
-
|
|
118
|
+
|
|
85
119
|
def push(self, serial: str, local: StrOrPath, remote: StrOrPath) -> str:
|
|
86
120
|
if isinstance(remote, Path):
|
|
87
121
|
remote = remote.as_posix()
|
|
88
122
|
command = f"{self.hdc} -t {serial} file send {local} {remote}"
|
|
89
123
|
return run_command(command)
|
|
90
|
-
|
|
124
|
+
|
|
91
125
|
def screenshot(self, serial: str) -> Image.Image:
|
|
92
126
|
device_path = f'/data/local/tmp/screenshot-{int(time.time()*1000)}.png'
|
|
93
127
|
self.shell(serial, f"uitest screenCap -p {device_path}")
|
|
@@ -116,7 +150,7 @@ class HDC:
|
|
|
116
150
|
raise HDCError(f"failed to dump layout: {output}")
|
|
117
151
|
finally:
|
|
118
152
|
self.shell(serial, f"rm {remote_path}")
|
|
119
|
-
|
|
153
|
+
|
|
120
154
|
|
|
121
155
|
class HarmonyDriver(BaseDriver):
|
|
122
156
|
def __init__(self, hdc: HDC, serial: str):
|
|
@@ -164,6 +198,112 @@ class HarmonyDriver(BaseDriver):
|
|
|
164
198
|
else:
|
|
165
199
|
return None
|
|
166
200
|
|
|
201
|
+
def get_app_info(self, package_name: str) -> Dict:
|
|
202
|
+
"""
|
|
203
|
+
Get detailed information about a specific application.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
package_name (str): The package name of the application to retrieve information for.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Dict: A dictionary containing the application information. If an error occurs during parsing,
|
|
210
|
+
an empty dictionary is returned.
|
|
211
|
+
"""
|
|
212
|
+
app_info = {}
|
|
213
|
+
data = self.hdc.shell(self.serial, f"bm dump -n {package_name}")
|
|
214
|
+
output = data
|
|
215
|
+
try:
|
|
216
|
+
json_start = output.find("{")
|
|
217
|
+
json_end = output.rfind("}") + 1
|
|
218
|
+
json_output = output[json_start:json_end]
|
|
219
|
+
|
|
220
|
+
app_info = json.loads(json_output)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"An error occurred: {e}")
|
|
223
|
+
return app_info
|
|
224
|
+
|
|
225
|
+
def get_app_abilities(self, package_name: str) -> List[Dict]:
|
|
226
|
+
"""
|
|
227
|
+
Get the abilities of an application.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
package_name (str): The package name of the application.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
List[Dict]: A list of dictionaries containing the abilities of the application.
|
|
234
|
+
"""
|
|
235
|
+
result = []
|
|
236
|
+
app_info = self.get_app_info(package_name)
|
|
237
|
+
hap_module_infos = app_info.get("hapModuleInfos")
|
|
238
|
+
main_entry = app_info.get("mainEntry")
|
|
239
|
+
for hap_module_info in hap_module_infos:
|
|
240
|
+
# 尝试读取moduleInfo
|
|
241
|
+
try:
|
|
242
|
+
ability_infos = hap_module_info.get("abilityInfos")
|
|
243
|
+
module_main = hap_module_info["mainAbility"]
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.warning(f"Fail to parse moduleInfo item, {repr(e)}")
|
|
246
|
+
continue
|
|
247
|
+
# 尝试读取abilityInfo
|
|
248
|
+
for ability_info in ability_infos:
|
|
249
|
+
try:
|
|
250
|
+
is_launcher_ability = False
|
|
251
|
+
skills = ability_info['skills']
|
|
252
|
+
if len(skills) > 0 and "action.system.home" in skills[0]["actions"]:
|
|
253
|
+
is_launcher_ability = True
|
|
254
|
+
icon_ability_info = {
|
|
255
|
+
"name": ability_info["name"],
|
|
256
|
+
"moduleName": ability_info["moduleName"],
|
|
257
|
+
"moduleMainAbility": module_main,
|
|
258
|
+
"mainModule": main_entry,
|
|
259
|
+
"isLauncherAbility": is_launcher_ability
|
|
260
|
+
}
|
|
261
|
+
result.append(icon_ability_info)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
logger.warning(f"Fail to parse ability_info item, {repr(e)}")
|
|
264
|
+
continue
|
|
265
|
+
logger.debug(f"all abilities: {result}")
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
def get_app_main_ability(self, package_name: str) -> Dict:
|
|
269
|
+
"""
|
|
270
|
+
Get the main ability of an application.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
package_name (str): The package name of the application to retrieve information for.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dict: A dictionary containing the main ability of the application.
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
if not (abilities := self.get_app_abilities(package_name)):
|
|
280
|
+
return {}
|
|
281
|
+
for item in abilities:
|
|
282
|
+
score = 0
|
|
283
|
+
if (name := item["name"]) and name == item["moduleMainAbility"]:
|
|
284
|
+
score += 1
|
|
285
|
+
if (module_name := item["moduleName"]) and module_name == item["mainModule"]:
|
|
286
|
+
score += 1
|
|
287
|
+
item["score"] = score
|
|
288
|
+
abilities.sort(key=lambda x: (not x["isLauncherAbility"], -x["score"]))
|
|
289
|
+
logger.debug(f"main ability: {abilities[0]}")
|
|
290
|
+
return abilities[0]
|
|
291
|
+
|
|
292
|
+
def app_launch(self, package: str, page_name: Optional[str] = None):
|
|
293
|
+
"""
|
|
294
|
+
Start an application on the device.
|
|
295
|
+
If the `page_name` is empty, it will retrieve the main ability using `get_app_main_ability`.
|
|
296
|
+
Args:
|
|
297
|
+
package (str): The package name of the application.
|
|
298
|
+
page_name (Optional[str]): Ability Name within the application to start. If not provided, the main ability will be used.
|
|
299
|
+
"""
|
|
300
|
+
if not page_name:
|
|
301
|
+
page_name = self.get_app_main_ability(package).get('name', 'MainAbility')
|
|
302
|
+
self.shell(f"aa start -a {page_name} -b {package}")
|
|
303
|
+
|
|
304
|
+
def app_terminate(self, package: str):
|
|
305
|
+
self.shell(f"aa force-stop {package}")
|
|
306
|
+
|
|
167
307
|
def shell(self, command: str) -> ShellResponse:
|
|
168
308
|
result = self.hdc.shell(self.serial, command)
|
|
169
309
|
return ShellResponse(output=result)
|
|
@@ -46,12 +46,18 @@ class AndroidProvider(BaseProvider):
|
|
|
46
46
|
def list_devices(self) -> list[DeviceInfo]:
|
|
47
47
|
adb = adbutils.AdbClient()
|
|
48
48
|
ret: list[DeviceInfo] = []
|
|
49
|
-
for d in adb.list():
|
|
49
|
+
for d in adb.list(extended=True):
|
|
50
50
|
if d.state != "device":
|
|
51
51
|
ret.append(DeviceInfo(serial=d.serial, status=d.state, enabled=False))
|
|
52
52
|
else:
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
ret.append(DeviceInfo(
|
|
54
|
+
serial=d.serial,
|
|
55
|
+
status=d.state,
|
|
56
|
+
name=d.tags.get('device', ''),
|
|
57
|
+
model=d.tags.get('model', ''),
|
|
58
|
+
product=d.tags.get('product', ''),
|
|
59
|
+
enabled=True
|
|
60
|
+
))
|
|
55
61
|
return ret
|
|
56
62
|
|
|
57
63
|
@lru_cache
|
|
@@ -76,7 +82,7 @@ class HarmonyProvider(BaseProvider):
|
|
|
76
82
|
|
|
77
83
|
def list_devices(self) -> list[DeviceInfo]:
|
|
78
84
|
devices = self.hdc.list_device()
|
|
79
|
-
return [DeviceInfo(serial=d, model=self.hdc.get_model(d), name=self.hdc.
|
|
85
|
+
return [DeviceInfo(serial=d, model=self.hdc.get_model(d), name=self.hdc.get_name(d)) for d in devices]
|
|
80
86
|
|
|
81
87
|
@lru_cache
|
|
82
88
|
def get_device_driver(self, serial: str) -> HarmonyDriver:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{uiautodev-0.11.0 → uiautodev-0.11.1}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|