diff --git a/hmdriver2/driver.py b/hmdriver2/driver.py index 179d10e..5d588cc 100644 --- a/hmdriver2/driver.py +++ b/hmdriver2/driver.py @@ -117,8 +117,11 @@ def install_app(self, apk_path: str): def uninstall_app(self, package_name: str): self.hdc.uninstall(package_name) - def list_apps(self) -> List: - return self.hdc.list_apps() + def list_apps(self, include_system_apps: bool = False) -> List: + return self.hdc.list_apps(include_system_apps) + + def app_version(self, bundle_name) -> Dict: + return self.hdc.app_version(bundle_name) def has_app(self, package_name: str) -> bool: return self.hdc.has_app(package_name) @@ -342,22 +345,20 @@ def push_file(self, lpath: str, rpath: str): """ self.hdc.send_file(lpath, rpath) - def screenshot(self, path: str) -> str: + def screenshot(self, path: str, method: str = "snapshot_display") -> str: """ Take a screenshot of the device display. Args: path (str): The local path to save the screenshot. + method (str): The screenshot method to use. Options are: + - "snapshot_display" (default, recommended for better performance) + - "screenCap" (alternative method, higher quality but slower). Returns: str: The path where the screenshot is saved. """ - _uuid = uuid.uuid4().hex - _tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg" - self.shell(f"snapshot_display -f {_tmp_path}") - self.pull_file(_tmp_path, path) - self.shell(f"rm -rf {_tmp_path}") # remove local path - return path + return self.hdc.screenshot(path, method=method) def shell(self, cmd) -> CommandResult: return self.hdc.shell(cmd) @@ -486,3 +487,4 @@ def xpath(self): """ from ._xpath import _XPath return _XPath(self) + diff --git a/hmdriver2/hdc.py b/hmdriver2/hdc.py index 000133f..e1ff09c 100644 --- a/hmdriver2/hdc.py +++ b/hmdriver2/hdc.py @@ -6,7 +6,7 @@ import re import os import subprocess -from typing import Union, List, Dict, Tuple +from typing import Union, List, Dict, Tuple, Optional from . import logger from .utils import FreePort @@ -139,10 +139,64 @@ def install(self, apkpath: str): raise HdcError("HDC install error", result.error) return result - def list_apps(self) -> List[str]: - result = self.shell("bm dump -a") + def list_apps(self, include_system_apps: bool = False) -> List[str]: + """ + List installed applications on the device. (Lazy loading, default: third-party apps) + + Args: + include_system_apps (bool): If True, include system apps in the list. + If False, only list third-party apps. + + Returns: + List[str]: A list of application package names. + + Note: + - When include_system_apps is False, the list typically contains around 50 third-party apps. + - When include_system_apps is True, the list typically contains around 200 apps in total. + """ + # Construct the shell command based on the include_system_apps flag + if include_system_apps: + command = "bm dump -a" + else: + command = "bm dump -a | grep -v 'com.huawei'" + + # Execute the shell command + result = self.shell(command) raw = result.output.split('\n') - return [item.strip() for item in raw] + + # Filter out strings starting with 'ID:' and empty strings + return [item.strip() for item in raw if item.strip() and not re.match(r'^ID:', item.strip())] + + def app_version(self, bundlename: str) -> Dict[str, Optional[str]]: + """ + Get the version information of an app installed on the device. + + Args: + bundlename (str): The bundle name of the app. + + Returns: + dict: A dictionary containing the version information: + - "versionName": The version name of the app. + - "versionCode": The version code of the app. + """ + result = _execute_command(f"{self.hdc_prefix} -t {self.serial} shell bm dump -n {bundlename} | grep '\"versionCode\":\\|versionName\"'") + + matches = re.findall(r'"versionCode":\s*(\d+),\s*"versionName":\s*"([^"]*)"', result.output) + if not matches: + return dict( + version_name='', + version_code='' + ) + + # Select the last match + version_code, version_name = matches[-1] + version_code = int(version_code) if version_code.isdigit() else None + version_name = version_name if version_name != "" else None + + return dict( + version_name=version_name, + version_code=version_code + ) def has_app(self, package_name: str) -> bool: data = self.shell("bm dump -a").output @@ -260,12 +314,38 @@ def swipe(self, x1, y1, x2, y2, speed=1000): def input_text(self, x: int, y: int, text: str): self.shell(f"uitest uiInput inputText {x} {y} {text}") - def screenshot(self, path: str) -> str: - _uuid = uuid.uuid4().hex - _tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg" - self.shell(f"snapshot_display -f {_tmp_path}") - self.recv_file(_tmp_path, path) - self.shell(f"rm -rf {_tmp_path}") # remove local path + def screenshot(self, path: str, method: str = "snapshot_display") -> str: + """ + Take a screenshot using one of the two available methods. + + Args: + path (str): The local path where the screenshot will be saved. + method (str): The screenshot method to use. Options are: + - "snapshot_display" (default, recommended for better performance) + This method is faster and more efficient, but the image quality is lower. + - "screenCap" (alternative method) + This method produces higher-quality images (5~20 times clearer), but it is slower. + + Returns: + str: The local path where the screenshot is saved. + """ + if method == "snapshot_display": + # Use the recommended method (snapshot_display) + _uuid = uuid.uuid4().hex + _tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg" + self.shell(f"snapshot_display -f {_tmp_path}") + self.recv_file(_tmp_path, path) + self.shell(f"rm -rf {_tmp_path}") + elif method == "screenCap": + # Use the alternative method (screenCap) + _uuid = uuid.uuid4().hex + _tmp_path = f"/data/local/tmp/{_uuid}.png" + self.shell(f"uitest screenCap -p {_tmp_path}") + self.recv_file(_tmp_path, path) + self.shell(f"rm -rf {_tmp_path}") + else: + raise ValueError(f"Invalid screenshot method: {method}. Use 'snapshot_display' or 'screenCap'.") + return path def dump_hierarchy(self) -> Dict: diff --git a/tests/test_driver.py b/tests/test_driver.py index 512ca6a..4fd4cea 100644 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -51,7 +51,40 @@ def test_uninstall_app(d): def test_list_apps(d): - assert "com.samples.test.uitest" in d.list_apps() + apps = d.list_apps() + # Assert that no item in the list starts with "ID:" + assert not any(item.startswith("ID:") for item in apps) + + # Assert that no empty strings are in the list + assert "" not in apps + + # Assert that "com.samples.test.uitest" is in the list + assert "com.sogou.input" in apps + # assert "com.samples.test.uitest" in apps + + # Test all apps (include system apps) + all_apps = d.list_apps(include_system_apps=True) + + # Assert that "com.huawei.systemmanager" is in the list (system app) + assert "com.huawei.hmos.settings" in all_apps, "'com.huawei.hmos.settings' is not in the full apps list." + + # Assert that "com.sogou.input" is also in the list + assert "com.sogou.input" in all_apps, "'com.sogou.input' is not in the full apps list." + + # Assert that the total number of apps is greater than the number of third-party apps + assert len(all_apps) > len(apps), "Full apps list is not larger than third-party apps list." + + +def test_app_version(d): + version_info = d.app_version('com.sogou.input') + + # Assert that version_name and version_code exist in the returned dictionary + assert 'version_name' in version_info, "version_name is missing in the returned dictionary." + assert 'version_code' in version_info, "version_code is missing in the returned dictionary." + + # Assert specific values + assert version_info['version_name'] == "1.0.4", "version_name does not match expected value." + assert version_info['version_code'] == 5, "version_code does not match expected value." def test_has_app(d): @@ -123,12 +156,18 @@ def test_push_file(d): d.push_file(lpath, rpath) -def test_screenshot(d) -> str: +def test_screenshot(d): lpath = "./test.png" d.screenshot(lpath) assert os.path.exists(lpath) +def test_screenshot_by_screen_cap(d): + lpath = "./test_screen_cap.png" + d.screenshot(lpath, method='screenCap') + assert os.path.exists(lpath) + + def test_shell(d): d.shell("pwd") @@ -176,7 +215,9 @@ def test_toast(d): def test_gesture(d): d(id="drag").click() - d.gesture.start(630, 984, interval=1).move(0.2, 0.4, interval=.5).pause(interval=1).move(0.5, 0.6, interval=.5).pause(interval=1).action() + d.gesture.start(630, 984, interval=1).move(0.2, 0.4, interval=.5).pause(interval=1).move(0.5, 0.6, + interval=.5).pause( + interval=1).action() d.go_back() @@ -213,7 +254,7 @@ def test_screenrecord2(d): def test_xpath(d): d.force_start_app("com.samples.test.uitest", "EntryAbility") - xpath1 = '//root[1]/Row[1]/Column[1]/Row[1]/Button[3]' # showToast + xpath1 = '//root[1]/Row[1]/Column[1]/Row[1]/Button[3]' # showToast xpath2 = '//*[@text="showDialog"]' d.toast_watcher.start() @@ -222,4 +263,4 @@ def test_xpath(d): print(f"toast: {toast}") assert toast == "testMessage" - d.xpath(xpath2).click() \ No newline at end of file + d.xpath(xpath2).click()