diff --git a/.github/workflows/cr.yml b/.github/workflows/cr.yml new file mode 100644 index 00000000..95c683c7 --- /dev/null +++ b/.github/workflows/cr.yml @@ -0,0 +1,23 @@ +name: Code Review + +permissions: + contents: read + pull-requests: write + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: anc95/ChatGPT-CodeReview@main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + # Optional + LANGUAGE: Chinese + MODEL: + top_p: 1 + temperature: 1 \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..5693d016 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.6" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: DocBuilder/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: DocBuilder/requirements.txt \ No newline at end of file diff --git a/DocBuilder/index.rst b/DocBuilder/index.rst index da3bf1bd..63ed1f9f 100644 --- a/DocBuilder/index.rst +++ b/DocBuilder/index.rst @@ -238,7 +238,7 @@ UI hierarchy. from poco.drivers.unity3d import UnityPoco as Poco poco = Poco() - ui = poco.agent.hierarchy.dump() + ui = poco.dump() # equivalent to poco.agent.hierarchy.dump() print(json.dumps(ui, indent=4)) diff --git a/doc/drivers/unity3d.rst b/doc/drivers/unity3d.rst index 186317a5..34160e98 100644 --- a/doc/drivers/unity3d.rst +++ b/doc/drivers/unity3d.rst @@ -134,4 +134,75 @@ If you are going to control multiple devices in the same test case, please follo ui2.swipe('up') +Integrating and Using Poco Interface Functions in Unity +------------------------------------------------------- + +This document serves as a guide for integrating and using the new ``UnityPoco.sendMessage()`` and ``UnityPoco.invoke()`` functions in your Unity project. +These functions facilitate communication between your Unity game and Poco, allowing for simple calls with single string arguments or calls with custom arguments. + +Getting Started +```````````````` + +Before using the new interfaces, ensure that you have the latest version of the Poco SDK that includes the changelog updates mentioned. This functionality relies on the updates provided in https://github.com/AirtestProject/Poco-SDK/pull/123. + +Using the ``sendMessage()`` Function +````````````````````````````````````` + +The ``UnityPoco.sendMessage()`` function allows you to send simple messages with a single string argument from Poco to Unity. + +Unity-side +~~~~~~~~~~ + +Implement OnPocoMessageReceived and add it to PocoManager.MessageReceived. This function will be called when a message is received from Poco. + +.. code-block:: csharp + + PocoManager.MessageReceived += OnPocoMessageReceived; + + +Poco-side +~~~~~~~~~ + +To use the ``sendMessage()`` function on the Poco side, you just need to call it and pass the message. + +.. code-block:: python + + poco = UnityPoco() + poco.sendMessage("Your message here") + + + +Using the ``invoke()`` Function +``````````````````````````````` + +The ``UnityPoco.invoke()`` function allows for more complex interactions with custom arguments. + +Poco-side +~~~~~~~~~ + +To use the ``invoke()`` function on the Poco side, you'll need to specify the listener and the arguments you want to pass. + +.. code-block:: python + + poco = UnityPoco() + poco.invoke(listener="say_hello", name="anonymous", year=2024) + +Unity-side +~~~~~~~~~~ + +On the Unity side, set up a method that will be called when ``invoke()`` is used from Poco. + +1. Create a class that derives from ``PocoListenerBase``. +2. Add a method that corresponds to the ``invoke()`` call: + + .. code-block:: csharp + + [PocoMethod("say_hello")] + public void SayHello(string name, int year) + { + Debug.Log($"Hi, {name}! The year {year} is coming soon!"); + } + +3. Add a reference to the new class in the ``PocoManager`` so that it knows to listen for calls to the ``say_hello`` method. + .. _airtest.core.api.connect_device: https://airtest.readthedocs.io/en/latest/all_module/airtest.core.api.html#airtest.core.api.connect_device \ No newline at end of file diff --git a/poco/drivers/android/lib/pocoservice-debug.apk b/poco/drivers/android/lib/pocoservice-debug.apk index e9e981b1..204e400c 100644 Binary files a/poco/drivers/android/lib/pocoservice-debug.apk and b/poco/drivers/android/lib/pocoservice-debug.apk differ diff --git a/poco/drivers/android/uiautomation.py b/poco/drivers/android/uiautomation.py index 77c8ce1e..a684e375 100644 --- a/poco/drivers/android/uiautomation.py +++ b/poco/drivers/android/uiautomation.py @@ -9,9 +9,8 @@ import threading import atexit -from airtest.core.api import connect_device, device as current_device from airtest.core.android.ime import YosemiteIme -from airtest.core.error import AdbShellError +from airtest.core.error import AdbShellError, AirtestError from hrpc.client import RpcClient from hrpc.transport.http import HttpTransport @@ -22,6 +21,7 @@ from poco.utils.hrpc.hierarchy import RemotePocoHierarchy from poco.utils.airtest.input import AirtestInput from poco.utils import six +from poco.utils.device import default_device from poco.drivers.android.utils.installation import install, uninstall __all__ = ['AndroidUiautomationPoco', 'AndroidUiautomationHelper'] @@ -113,10 +113,6 @@ def stopped(self): def run(self): while not self.stopped(): - if getattr(self.poco, "_instrument_proc", None) is not None: - stdout, stderr = self.poco._instrument_proc.communicate() - print('[pocoservice.apk] stdout: {}'.format(stdout)) - print('[pocoservice.apk] stderr: {}'.format(stderr)) if not self.stopped(): self.poco._start_instrument(self.port_to_ping) # 尝试重启 time.sleep(1) @@ -150,9 +146,7 @@ def __init__(self, device=None, using_proxy=True, force_restart=False, use_airte if options.get('screenshot_each_action') is False: self.screenshot_each_action = False - self.device = device or current_device() - if not self.device: - self.device = connect_device("Android:///") + self.device = device or default_device() self.adb_client = self.device.adb if using_proxy: @@ -161,7 +155,12 @@ def __init__(self, device=None, using_proxy=True, force_restart=False, use_airte self.device_ip = self.device.get_ip_address() # save current top activity (@nullable) - current_top_activity_package = self.device.get_top_activity_name() + try: + current_top_activity_package = self.device.get_top_activity_name() + except AirtestError as e: + # 在一些极端情况下,可能获取不到top activity的信息 + print(e) + current_top_activity_package = None if current_top_activity_package is not None: current_top_activity_package = current_top_activity_package.split('/')[0] @@ -173,9 +172,11 @@ def __init__(self, device=None, using_proxy=True, force_restart=False, use_airte self._install_service() # forward + self.forward_list = [] if using_proxy: p0, _ = self.adb_client.setup_forward("tcp:10080") p1, _ = self.adb_client.setup_forward("tcp:10081") + self.forward_list.extend(["tcp:%s" % p0, "tcp:%s" % p1]) else: p0 = 10080 p1 = 10081 @@ -251,9 +252,9 @@ def _start_instrument(self, port_to_ping, force_restart=False): self.adb_client.shell('am start -n {}/.TestActivity'.format(PocoServicePackage)) instrumentation_cmd = [ - 'am', 'instrument', '-w', '-e', 'debug', 'false', '-e', 'class', - '{}.InstrumentedTestAsLauncher'.format(PocoServicePackage), - '{}/androidx.test.runner.AndroidJUnitRunner'.format(PocoServicePackage)] + 'am', 'instrument', '-w', '-e', 'debug', 'false', '-e', 'class', + '{}.InstrumentedTestAsLauncher'.format(PocoServicePackage), + '{}/androidx.test.runner.AndroidJUnitRunner'.format(PocoServicePackage)] self._instrument_proc = self.adb_client.start_shell(instrumentation_cmd) def cleanup_proc(proc): @@ -282,7 +283,17 @@ def wrapped(): print('[pocoservice.apk] stderr: {}'.format(stderr)) time.sleep(1) print("still waiting for uiautomation ready.") + try: + self.adb_client.shell( + ['monkey', '-p', {PocoServicePackage}, '-c', 'android.intent.category.LAUNCHER', '1']) + except Exception as e: + pass self.adb_client.shell('am start -n {}/.TestActivity'.format(PocoServicePackage)) + instrumentation_cmd = [ + 'am', 'instrument', '-w', '-e', 'debug', 'false', '-e', 'class', + '{}.InstrumentedTestAsLauncher'.format(PocoServicePackage), + '{}/androidx.test.runner.AndroidJUnitRunner'.format(PocoServicePackage)] + self._instrument_proc = self.adb_client.start_shell(instrumentation_cmd) continue return ready @@ -318,8 +329,14 @@ def stop_running(self): print('[pocoservice.apk] stopping PocoService') self._keep_running_thread.stop() self._keep_running_thread.join(3) + self.remove_forwards() self.adb_client.shell(['am', 'force-stop', PocoServicePackage]) + def remove_forwards(self): + for p in self.forward_list: + self.adb_client.remove_forward(p) + self.forward_list = [] + class AndroidUiautomationHelper(object): _nuis = {} diff --git a/poco/drivers/cocosjs/__init__.py b/poco/drivers/cocosjs/__init__.py index b36c8a97..54ff5ebe 100644 --- a/poco/drivers/cocosjs/__init__.py +++ b/poco/drivers/cocosjs/__init__.py @@ -17,6 +17,7 @@ from urlparse import urlparse from airtest.core.api import connect_device, device as current_device +from poco.utils.device import default_device from airtest.core.helper import device_platform @@ -26,25 +27,23 @@ class CocosJsPocoAgent(PocoAgent): - def __init__(self, port, device=None): - self.device = device or current_device() - if not self.device: - self.device = connect_device("Android:///") - - platform_name = device_platform(self.device) - if platform_name == 'Android': - local_port, _ = self.device.adb.setup_forward('tcp:{}'.format(port)) - ip = self.device.adb.host or 'localhost' - port = local_port - elif platform_name == 'IOS': - # Note: ios is now support for now. - # ip = device.get_ip_address() - # use iproxy first - ip = 'localhost' - local_port, _ = self.device.instruct_helper.setup_proxy(port) - port = local_port - else: - ip = self.device.get_ip_address() + def __init__(self, port, device=None, ip=None): + if ip is None or ip == "localhost": + self.device = device or default_device() + + platform_name = device_platform(self.device) + if platform_name == 'Android': + local_port, _ = self.device.adb.setup_forward('tcp:{}'.format(port)) + ip = self.device.adb.host or 'localhost' + port = local_port + elif platform_name == 'IOS': + port, _ = self.device.setup_forward(port) + if self.device.is_local_device: + ip = 'localhost' + else: + ip = self.device.ip + else: + ip = self.device.get_ip_address() # transport self.conn = WebSocketClient('ws://{}:{}'.format(ip, port)) @@ -90,11 +89,12 @@ def __init__(self, addr=DEFAULT_ADDR, device=None, **options): port = urlparse(addr).port if not port: raise ValueError + ip = urlparse(addr).hostname except ValueError: raise ValueError('Argument "addr" should be a tuple[2] or string format. e.g. ' '["localhost", 5003] or "ws://localhost:5003". Got {}'.format(repr(addr))) - agent = CocosJsPocoAgent(port, device) + agent = CocosJsPocoAgent(port, device, ip=ip) if 'action_interval' not in options: options['action_interval'] = 0.5 super(CocosJsPoco, self).__init__(agent, **options) diff --git a/poco/drivers/ios/__init__.py b/poco/drivers/ios/__init__.py index 102dde59..08366fa9 100644 --- a/poco/drivers/ios/__init__.py +++ b/poco/drivers/ios/__init__.py @@ -41,9 +41,16 @@ def __init__(self, client): self.size = client.display_info["window_width"], client.display_info["window_height"] def dumpHierarchy(self, onlyVisibleNode=True): - switch_flag = False - if self.client.is_pad and self.client.orientation != 'PORTRAIT' and self.client.home_interface(): + # 当使用了appium/WebDriverAgent时,ipad横屏且在桌面下,坐标需要按照竖屏坐标额外转换一次 + # 判断条件如下: + # 当ios.using_ios_tagent有值、且为false,说明使用的是appium/WebDriverAgent + # airtest<=1.2.4时,没有ios.using_ios_tagent的值,说明也是用的appium/wda + if ((hasattr(self.client, "using_ios_tagent") and not self.client.using_ios_tagent) or + (not hasattr(self.client, "using_ios_tagent"))) \ + and (self.client.is_pad and self.client.orientation != 'PORTRAIT' and self.client.home_interface()): switch_flag = True + else: + switch_flag = False jsonObj = self.client.driver.source(format='json') w, h = self.size if self.client.orientation in ['LANDSCAPE', 'UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT']: diff --git a/poco/drivers/netease/internal.py b/poco/drivers/netease/internal.py index 2c32057b..8429fd53 100644 --- a/poco/drivers/netease/internal.py +++ b/poco/drivers/netease/internal.py @@ -17,7 +17,7 @@ class NeteasePocoAgent(PocoAgent): def __init__(self, hunter): - client = HunterRpcClient(hunter) + client = hunter.rpc client.set_timeout(25) remote_poco = client.remote('poco-uiautomation-framework-2') @@ -42,10 +42,10 @@ def __init__(self, hunter): class NeteasePoco(Poco): def __init__(self, process, hunter=None, **options): - apitoken = open_platform.get_api_token(process) if hunter: self._hunter = hunter else: + apitoken = open_platform.get_api_token(process) self._hunter = AirtestHunter(apitoken, process) agent = NeteasePocoAgent(self._hunter) super(NeteasePoco, self).__init__(agent, **options) diff --git a/poco/drivers/std/__init__.py b/poco/drivers/std/__init__.py index dcee9dd2..c0d97a69 100644 --- a/poco/drivers/std/__init__.py +++ b/poco/drivers/std/__init__.py @@ -11,6 +11,7 @@ from poco.utils.simplerpc.rpcclient import RpcClient from poco.utils.simplerpc.transport.tcp.main import TcpClient from poco.utils.simplerpc.utils import sync_wrapper +from poco.utils.device import default_device from airtest.core.api import connect_device, device as current_device from airtest.core.helper import device_platform @@ -70,6 +71,9 @@ class StdPoco(Poco): device = connect_device('Android:///') poco = StdPoco(10054, device) + # or use ip:port to initialize poco object + poco = StdPoco(port=10054, ip='xx.xx.xx.xx') + # now you can play with poco ui = poco('...') ui.click() @@ -77,31 +81,31 @@ class StdPoco(Poco): """ - def __init__(self, port=DEFAULT_PORT, device=None, use_airtest_input=True, **kwargs): - self.device = device or current_device() - if not self.device: - self.device = connect_device("Android:///") - - platform_name = device_platform(self.device) - if platform_name == 'Android': - # always forward for android device to avoid network unreachable - local_port, _ = self.device.adb.setup_forward('tcp:{}'.format(port)) - ip = self.device.adb.host or 'localhost' - port = local_port - elif platform_name == 'IOS': - # ip = device.get_ip_address() - # use iproxy first - ip = 'localhost' - port, _ = self.device.instruct_helper.setup_proxy(port) - else: - try: - ip = self.device.get_ip_address() - except AttributeError: - try: - ip = socket.gethostbyname(socket.gethostname()) - except socket.gaierror: - # 某些特殊情况下会出现这个error,无法正确获取本机ip地址 + def __init__(self, port=DEFAULT_PORT, device=None, use_airtest_input=True, ip=None, **kwargs): + if ip is None or ip == "localhost": + self.device = device or default_device() + + platform_name = device_platform(self.device) + if platform_name == 'Android': + # always forward for android device to avoid network unreachable + local_port, _ = self.device.adb.setup_forward('tcp:{}'.format(port)) + ip = self.device.adb.host or 'localhost' + port = local_port + elif platform_name == 'IOS': + port, _ = self.device.setup_forward(port) + if self.device.is_local_device: ip = 'localhost' + else: + ip = self.device.ip + else: + try: + ip = self.device.get_ip_address() + except AttributeError: + try: + ip = socket.gethostbyname(socket.gethostname()) + except socket.gaierror: + # 某些特殊情况下会出现这个error,无法正确获取本机ip地址 + ip = 'localhost' agent = StdPocoAgent((ip, port), use_airtest_input) kwargs['reevaluate_volatile_attributes'] = True diff --git a/poco/drivers/ue4/ue4_poco.py b/poco/drivers/ue4/ue4_poco.py index e04b73d4..cad3ed4a 100644 --- a/poco/drivers/ue4/ue4_poco.py +++ b/poco/drivers/ue4/ue4_poco.py @@ -23,4 +23,4 @@ def __init__(self, addr=DEFAULT_ADDR, ue4_editor=False, connect_default_device=T if dev is None and connect_default_device and not current_device(): dev = connect_device("Android:///") - super(UE4Poco, self).__init__(addr[1], dev, **options) + super(UE4Poco, self).__init__(addr[1], dev, ip=addr[0], **options) diff --git a/poco/drivers/unity3d/unity3d_poco.py b/poco/drivers/unity3d/unity3d_poco.py index aa4b3d76..47713d4a 100644 --- a/poco/drivers/unity3d/unity3d_poco.py +++ b/poco/drivers/unity3d/unity3d_poco.py @@ -77,6 +77,19 @@ def __init__(self, addr=DEFAULT_ADDR, unity_editor=False, connect_default_device # can apply auto detection in the future dev = connect_device("Android:///") - super(UnityPoco, self).__init__(addr[1], dev, **options) + super(UnityPoco, self).__init__(addr[1], dev, ip=addr[0], **options) # If some devices fail to initialize, the UI tree cannot be obtained # self.vr = UnityVRSupport(self.agent.rpc) + + def send_message(self, message): + self.agent.rpc.call("SendMessage", message) + + def invoke(self, listener, **kwargs): + callback = self.agent.rpc.call("Invoke", listener=listener, data=kwargs) + + value, error = callback.wait() + + if error is not None: + raise Exception(error) + + return value diff --git a/poco/pocofw.py b/poco/pocofw.py index 9b741f37..8c025bf1 100644 --- a/poco/pocofw.py +++ b/poco/pocofw.py @@ -257,7 +257,9 @@ def rclick(self, pos): raise NotImplementedError def double_click(self, pos): - raise NotImplementedError + ret = self.agent.input.double_click(pos[0], pos[1]) + self.wait_stable() + return ret def swipe(self, p1, p2=None, direction=None, duration=2.0): """ @@ -505,3 +507,13 @@ def use_render_resolution(self, use=True, resolution=None): ''' self._agent.input.use_render_resolution = use self._agent.input.render_resolution = resolution + + def dump(self): + """ + Dump the current UI tree of the target device. The output format depends on the agent implementation. + + Returns: + :obj:`str`: base64 encoded UI tree data + """ + + return self.agent.hierarchy.dump() diff --git a/poco/proxy.py b/poco/proxy.py index 25393d11..696f3f2f 100644 --- a/poco/proxy.py +++ b/poco/proxy.py @@ -191,7 +191,7 @@ def sibling(self, name=None, **attrs): def parent(self): """ - Select the direct child(ren) from the UI element(s) given by the query expression, see ``QueryCondition`` for + Select the direct parent from the UI element(s) given by the query expression, see ``QueryCondition`` for more details about the selectors. Warnings: @@ -269,7 +269,7 @@ def __len__(self): nodes = [] else: nodes = self._nodes - return len(nodes) + return len(nodes) if nodes else 0 def __iter__(self): """ @@ -328,7 +328,7 @@ def click(self, focus=None, sleep_interval=None): PocoNoSuchNodeException: raised when the UI element does not exist """ - focus = focus or self._focus or 'anchor' + focus = focus or self._focus or 'center' pos_in_percentage = self.get_position(focus) self.poco.pre_action('click', self, pos_in_percentage) ret = self.poco.click(pos_in_percentage) @@ -360,7 +360,7 @@ def rclick(self, focus=None, sleep_interval=None): PocoNoSuchNodeException: raised when the UI element does not exist """ - focus = focus or self._focus or 'anchor' + focus = focus or self._focus or 'center' pos_in_percentage = self.get_position(focus) self.poco.pre_action('rclick', self, pos_in_percentage) ret = self.poco.rclick(pos_in_percentage) @@ -392,7 +392,7 @@ def double_click(self, focus=None, sleep_interval=None): PocoNoSuchNodeException: raised when the UI element does not exist """ - focus = focus or self._focus or 'anchor' + focus = focus or self._focus or 'center' pos_in_percentage = self.get_position(focus) self.poco.pre_action('double_click', self, pos_in_percentage) ret = self.poco.double_click(pos_in_percentage) @@ -422,7 +422,7 @@ def long_click(self, duration=2.0): except ValueError: raise ValueError('Argument `duration` should be . Got {}'.format(repr(duration))) - pos_in_percentage = self.get_position(self._focus or 'anchor') + pos_in_percentage = self.get_position(self._focus or 'center') self.poco.pre_action('long_click', self, pos_in_percentage) ret = self.poco.long_click(pos_in_percentage, duration) self.poco.post_action('long_click', self, pos_in_percentage) @@ -452,7 +452,7 @@ def swipe(self, direction, focus=None, duration=0.5): except ValueError: raise ValueError('Argument `duration` should be . Got {}'.format(repr(duration))) - focus = focus or self._focus or 'anchor' + focus = focus or self._focus or 'center' dir_vec = self._direction_vector_of(direction) origin = self.get_position(focus) self.poco.pre_action('swipe', self, (origin, dir_vec)) @@ -588,7 +588,6 @@ def focus(self, f): Returns: :py:class:`UIObjectProxy `: a new UI proxy object (copy) """ - ret = copy.copy(self) ret._focus = f return ret @@ -607,8 +606,7 @@ def get_position(self, focus=None): Raises: TypeError: raised when unsupported focus type is specified """ - - focus = focus or self._focus or 'anchor' + focus = focus or self._focus or 'center' if focus == 'anchor': pos = list(map(float, self.attr('pos'))) elif focus == 'center': @@ -696,6 +694,8 @@ def wait_for_disappearance(self, timeout=120): self.poco.sleep_for_polling_interval() if time.time() - start > timeout: raise PocoTargetTimeout('disappearance', self) + # 强制重新获取节点状态,避免节点已经存在、又消失后,这里不会刷新节点信息导致exists()永远为True的bug + self.invalidate() @refresh_when(PocoTargetRemovedException) def attr(self, name): @@ -862,15 +862,29 @@ def nodes(self): def invalidate(self): """ Clear the flag to indicate to re-query or re-select the UI element(s) from hierarchy. + + alias is refresh() + + Example: + >>> a = poco(text="settings") + >>> print(a.exists()) + >>> a.refresh() + >>> print(a.exists()) """ self._evaluated = False self._nodes = None + # refresh is alias of invalidate + # use poco(xxx).refresh() to force the UI element(s) to re-query + refresh = invalidate + def _do_query(self, multiple=True, refresh=False): if not self._evaluated or refresh: self._nodes = self.poco.agent.hierarchy.select(self.query, multiple) - if len(self._nodes) == 0: + if not self._nodes or len(self._nodes) == 0: + # 找不到节点时,将当前节点状态重置,强制下一次访问时重新查询一次节点信息 + self.invalidate() raise PocoNoSuchNodeException(self) self._evaluated = True self._query_multiple = multiple diff --git a/poco/sdk/exceptions.py b/poco/sdk/exceptions.py index 269235e6..70e908eb 100644 --- a/poco/sdk/exceptions.py +++ b/poco/sdk/exceptions.py @@ -61,7 +61,7 @@ def __init__(self, matchingMethod, matcherName): class NonuniqueSurfaceException(Exception): """ - Raised when the device selector matches mutiple devices surface + Raised when the device selector matches multiple devices surface """ def __init__(self, selector): @@ -75,6 +75,6 @@ class InvalidSurfaceException(Exception): """ def __init__(self, target, msg="None"): - msg = 'Target device surface invalid ("{}") . Detial message: "{}"'.format(target, msg) + msg = 'Target device surface invalid ("{}") . Detail message: "{}"'.format(target, msg) super(InvalidSurfaceException, self).__init__(msg) diff --git a/poco/sdk/interfaces/input.py b/poco/sdk/interfaces/input.py index 90d0d397..ceb5ab88 100644 --- a/poco/sdk/interfaces/input.py +++ b/poco/sdk/interfaces/input.py @@ -24,6 +24,17 @@ def click(self, x, y): raise NotImplementedError + def double_click(self, x, y): + """ + Perform click action as simulated input on target device. Coordinates arguments are all in range of 0~1. + + Args: + y (:obj:`float`): y-coordinate + x (:obj:`float`): x-coordinate + """ + + raise NotImplementedError + def swipe(self, x1, y1, x2, y2, duration): """ Perform swipe action as simulated input on target device from point A to B within given time interval to diff --git a/poco/utils/airtest/input.py b/poco/utils/airtest/input.py index 34119167..55c62417 100644 --- a/poco/utils/airtest/input.py +++ b/poco/utils/airtest/input.py @@ -3,7 +3,7 @@ from functools import wraps from airtest.core.api import device as current_device -from airtest.core.api import touch, swipe +from airtest.core.api import touch, swipe, double_click from airtest.core.helper import device_platform, logwrap from poco.sdk.interfaces.input import InputInterface @@ -80,6 +80,10 @@ def click(self, x, y): pos = self.get_target_pos(x, y) touch(pos, duration=self.default_touch_down_duration) + def double_click(self, x, y): + pos = self.get_target_pos(x, y) + double_click(pos) + def swipe(self, x1, y1, x2, y2, duration=2.0): if duration <= 0: raise ValueError("Operation duration cannot be less equal 0. Please provide a positive number.") diff --git a/poco/utils/device.py b/poco/utils/device.py index 1817491a..aa6a75d4 100644 --- a/poco/utils/device.py +++ b/poco/utils/device.py @@ -2,6 +2,8 @@ from __future__ import absolute_import from airtest.core.device import Device +from airtest.core.api import connect_device, device as current_device +from airtest.core.error import NoDeviceError class VirtualDevice(Device): @@ -18,3 +20,15 @@ def get_current_resolution(self): def get_ip_address(self): return self.ip + + +def default_device(): + """ + Get default device, if no device connected, connect to first android device. + + :return: + """ + try: + return current_device() + except NoDeviceError: + return connect_device('Android:///') diff --git a/poco/utils/simplerpc/jsonrpc/dispatcher.py b/poco/utils/simplerpc/jsonrpc/dispatcher.py index 2bf5e430..7771bb2c 100644 --- a/poco/utils/simplerpc/jsonrpc/dispatcher.py +++ b/poco/utils/simplerpc/jsonrpc/dispatcher.py @@ -3,10 +3,13 @@ For usage examples see :meth:`Dispatcher.add_method` """ -import collections +try: + from collections import MutableMapping # noqa +except ImportError: + from collections.abc import MutableMapping # noqa -class Dispatcher(collections.MutableMapping): +class Dispatcher(MutableMapping): """ Dictionary like object which maps method_name to method.""" diff --git a/poco/utils/simplerpc/transport/tcp/safetcp.py b/poco/utils/simplerpc/transport/tcp/safetcp.py index 8b3e6a6e..cebd50e1 100644 --- a/poco/utils/simplerpc/transport/tcp/safetcp.py +++ b/poco/utils/simplerpc/transport/tcp/safetcp.py @@ -2,7 +2,7 @@ import socket -DEFAULT_TIMEOUT = 2 +DEFAULT_TIMEOUT = 5 DEFAULT_SIZE = 4096 diff --git a/poco/utils/simplerpc/transport/ws/main.py b/poco/utils/simplerpc/transport/ws/main.py index 2a04f6dd..da0f7cc7 100644 --- a/poco/utils/simplerpc/transport/ws/main.py +++ b/poco/utils/simplerpc/transport/ws/main.py @@ -64,7 +64,7 @@ def _on_ws_error(self, ws, error): print("on error", error) self.on_close() - def _on_ws_close(self, ws): + def _on_ws_close(self, ws, *args, **kwargs): print("on close") self.on_close() diff --git a/setup.py b/setup.py index dd0d05cd..b29c321c 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ def parse_requirements(filename='requirements.txt'): setup( name='pocoui', - version='1.0.85', + version='1.0.94', keywords="poco, automation test, ui automation", description='Poco cross-engine UI automated test framework.', long_description='Poco cross-engine UI automated test framework. 2018 present by NetEase Games', @@ -20,6 +20,6 @@ def parse_requirements(filename='requirements.txt'): license='Apache License 2.0', author='Netease Games', - author_email='lxn3032@corp.netease.com, gzliuxin@corp.netease.com', + author_email='rockywhisper@163.com, lxhustauto@gmail.com', url='https://github.com/AirtestProject/Poco', )