diff --git a/robotframework_airtest/__init__.py b/robotframework_airtest/__init__.py index 8b13789..ac42450 100644 --- a/robotframework_airtest/__init__.py +++ b/robotframework_airtest/__init__.py @@ -1 +1,23 @@ +from .device import DeviceLibrary +from .poco import ( + AndroidUiAutomationPocoLibrary, + CocosJsPocoLibrary, + IOSPocoLibrary, + StdPocoLibrary, + UE4PocoLibrary, + UnityPocoLibrary, +) + +from .airtest import AirtestLibrary + +__all__ = [ + "AndroidUiAutomationPocoLibrary", + "CocosJsPocoLibrary", + "IOSPocoLibrary", + "StdPocoLibrary", + "UE4PocoLibrary", + "UnityPocoLibrary", + "AirtestLibrary", + "DeviceLibrary", +] diff --git a/robotframework_airtest/airtest/__init__.py b/robotframework_airtest/airtest/__init__.py index 6eceb7e..2a103ef 100644 --- a/robotframework_airtest/airtest/__init__.py +++ b/robotframework_airtest/airtest/__init__.py @@ -3,3 +3,403 @@ date: 2023-11-23 17:36:57 Copyright © Kaluluosi All rights reserved """ +import os +from airtest.core.api import ( + Template as AirTemplate, + touch, + wait, + swipe, + exists, + text, + keyevent, + snapshot, + sleep, + assert_exists, + assert_not_exists, + assert_equal, + assert_not_equal, + find_all, + get_clipboard, + set_clipboard, + pinch, + home, + wake, +) +from airtest.utils.transform import TargetPos + +from robot.libraries.BuiltIn import BuiltIn +from robot.api import deco +from robot.utils import timestr_to_secs + +from typing import Any, Callable, Literal, Optional, Tuple, Union + +Point = Tuple[float, float] +TargetType = Union["Template", Point, str, dict] + + +class Template(AirTemplate): + def __init__( + self, + filename: str, + threshold: Optional[float] = None, + target_pos=TargetPos.MID, + record_pos: Optional[Point] = None, + resolution=(), + rgb: bool = False, + scale_max: int = 800, + scale_step: float = 0.005, + ): + """ + Args: + filename (str): 文件名,相对于robot脚本的路径 \n + threshold (Optional[float], optional): 识别阈值,越高越严格. Defaults to None. \n + record_pos (Optional[Point], optional): 录制坐标. Defaults to None. \n + target_pos (int, optional): 点击目标坐标偏移. Defaults to TargetPos.MID. \n + resolution (tuple, optional): 分辨率. Defaults to (). \n + rgb (bool, optional): 是否rgb三通道识别,默认是将图片转单通道灰度图,灰度更快. Defaults to False. \n + scale_max (int, optional): 最大缩放倍数. Defaults to 800. \n + scale_step (float, optional): 缩放步进. Defaults to 0.005. \n + """ + self.filename = os.path.join(self.curdir, filename) + super().__init__( + self.filename, + threshold, + target_pos, + record_pos, + resolution, + rgb, + scale_max, + scale_step, + ) + + @property + def curdir(self): + suit_source = BuiltIn().get_variable_value("${SUITE SOURCE}") + curdir = os.path.dirname(suit_source) + return curdir + + +class AirtestLibrary: + ROBOT_LIBRARY_SCOPE = "TEST CASE" + ROBOT_LIBRARY_FORMAT = "rsST" + ROBOT_LISTENER_API_VERSION = 2 + + def __init__(self) -> None: + pass + + @property + def curdir(self): + suit_source = BuiltIn().get_variable_value("${SUITE SOURCE}") + curdir = os.path.dirname(suit_source) + return curdir + + def template( + self, + target: TargetType, + threshold: Optional[float] = None, + target_pos: int = TargetPos.MID, + record_pos: Optional[Point] = None, + resolution=(), + rgb: bool = False, + scale_max: int = 800, + scale_step: float = 0.005, + ): + """ + 创建Airtest Template对象 + + `Target` 的路径是`robot`脚本的相对路径,也就是说你只需要将图片放到 `robot` + 相同目录下然后就可以相对路径的方式引用。 + + 例子: + + test.robot、btn_start.png 这两个文件都在同一个目录, + 那么`Template`可以直接引用`btn_start.png`,不需要复杂的转换。 + + ```robotframework + ${开始按钮} Template btn_start.png + ``` + + + + Args: + target (TargetType): _description_ + threshold (Optional[float], optional): _description_. Defaults to None. + target_pos (int, optional): _description_. Defaults to TargetPos.MID. + record_pos (Optional[Point], optional): _description_. Defaults to None. + resolution (Any, optional): _description_. Defaults to None. + rgb (bool, optional): _description_. Defaults to False. + scale_max (int, optional): _description_. Defaults to 800. + scale_step (float, optional): _description_. Defaults to 0.005. + + Returns: + _type_: _description_ + """ + if isinstance(target, str): + return Template( + target, + threshold, + target_pos, + record_pos, + resolution, + rgb, + scale_max, + scale_step, + ) + elif isinstance(target, Template) or isinstance(target, Tuple): + return target + elif isinstance(target, dict) and "result" in target: + return target.get("result") + + @deco.keyword("点击") + def touch( + self, + target: TargetType, + times: int = 1, + ): + """ + 点击`Touch` + + NOTE: `threashold` 及其后面的参数可以无视,因为都会默认使用`airtest.core.settings:Settings`的配置。 + + Args: + target (TargetType): 目标可以是坐标`Tuple`,也可以是`Template`,也可以是文件名。 + times (int, optional): 点击次数. Defaults to 1. + """ + + v = self.template(target) + touch(v, times=times) + + @deco.keyword("等待") + def wait( + self, + target: TargetType, + timeout: str, + interval: float = 0.5, + interval_func: Optional[Callable] = None, + ): + """ + 等待目标出现 `Wait` + + Args: + target (TargetType): 目标可以是坐标`Tuple`,也可以是`Template`,也可以是文件名。 + timeout (str): 超时,支持`robot`的时间字符串,如'1h 10s', '01:00:10' and '42'等,具体可以看`timestr_to_secs`。 + interval (float, optional): 等待检查间隔. Defaults to 0.5. + interval_func (Optional[str], optional): 每次检查失败时调用回调. Defaults to None. + + """ + timeout_secs = timestr_to_secs(timeout) + v = self.template(target) + + if interval_func is not None: + + def _callback(): + return BuiltIn().run_keyword(interval_func) + + invoker = _callback + else: + invoker = None + + wait(v, timeout_secs, interval=interval, intervalfunc=invoker) + + @deco.keyword("滑动") + def swipe( + self, + target1: TargetType, + target2: Optional[TargetType] = None, + vector: Optional[Point] = None, + ): + """ + 滑动 `Swipe` + + 例子 + + ```robotframework + + 滑动 start_btn.png list_view + 滑动 start_btn.png (10,0) + 滑动 (10,10) vector=(20,20) + + ``` + + Args: + target1 (TargetType): 目标可以是坐标`Tuple`,也可以是`Template`,也可以是文件名。 + target2 (TargetType): 目标可以是坐标`Tuple`,也可以是`Template`,也可以是文件名。 + vector (Optional[Point], optional): 方向向量 如`(1,0)`-向右. Defaults to None. + """ + v1 = self.template( + target1, + ) + + v2 = None + if target2: + v2 = self.template( + target2, + ) + swipe(v1, v2, vector) + + @deco.keyword("存在") + def exists(self, target: TargetType) -> bool: + """ + 检查目标是否存在 + + NOTE: 不会引发异常 + + Args: + target (TargetType): 目标可以是坐标`Tuple`,也可以是`Template`,也可以是文件名。 + + Returns: + bool : 存在返回`true` + """ + v = self.template(target) + return exists(v) + + @deco.keyword("输入文字") + def text(self, _text: str, enter: bool = True): + """ + 输入文字 + + Args: + _text (str): 文字 + enter (bool, optional): 输入完后按回车. Defaults to True. + """ + text(_text, enter=enter) + + @deco.keyword("按键事件") + def keyevent(self, keyname: str, *args, **kwargs): + """ + 按键事件 + + Args: + keyname (str): 按键名或者按键码 + """ + keyevent(keyname, *args, **kwargs) + + @deco.keyword("截图") + def snapshot( + self, + filename: Optional[str] = None, + msg: str = "", + quality: Optional[int] = None, + max_size: Optional[int] = None, + ): + """ + 截图 + + Args: + filename (Optional[str], optional): 默认保存到LOG_DIR. Defaults to None. + msg (str, optional): 信息. Defaults to "". + quality (Optional[int], optional): 质量. Defaults to None. + max_size (Optional[int], optional): 最大尺寸. Defaults to None. + + Returns: + _type_: _description_ + """ + return snapshot(filename, msg=msg, quality=quality, max_size=max_size) + + @deco.keyword("睡眠") + def sleep(self, timeout: str): + """ + 睡眠 + + Args: + timeout (str): `robot`时间字符串,如`1h 2m 3s` `2min 3sec` `40 sec`等都能识别。 + """ + secs = timestr_to_secs(timeout) + sleep(secs) + + @deco.keyword("必须存在") + def assert_exists(self, target: TargetType, msg: str = ""): + """ + 断言 元素必须存在 + + Args: + target (TargetType): 目标可以是坐标`Tuple`,也可以是`Template`,也可以是文件名。 + msg (str, optional): 信息. Defaults to "". + """ + v = self.template(target) + assert_exists(v, msg) + + @deco.keyword("必须不存在") + def assert_not_exists(self, target: TargetType, msg: str = ""): + """ + 断言 元素必须不存在 + + + Args: + target (TargetType): 目标可以是坐标`Tuple`,也可以是`Template`,也可以是文件名。 + msg (str, optional): 信息. Defaults to "". + """ + v = self.template(target) + assert_not_exists(v, msg) + + @deco.keyword("必须相等") + def assert_equal( + self, + first: Any, + second: Any, + msg: str = "", + snapshot: bool = True, + ): + """ + 断言 必须相等 + + NOTE: 这个断言跟 `robot` 的断言区别在于这个会再Airtest日志中留截图日志 + + Args: + first (Any): 任何类型 + second (Any): 任何类型 + msg (str, optional): 信息. Defaults to "". + snapshot (bool, optional): 是否截图. Defaults to True. + """ + assert_equal(first, second, msg=msg, snapshot=snapshot) + + @deco.keyword("必须不相等") + def assert_not_equal( + self, + first: Any, + second: Any, + msg: str = "", + snapshot: bool = True, + ): + """ + 对比两个值或对象 + + NOTE: 这个断言跟 `robot` 的断言区别在于这个会再Airtest日志中留截图日志 + + Args: + first (Any): 任何类型 + second (Any): 任何类型 + msg (str, optional): 信息. Defaults to "". + snapshot (bool, optional): 是否截图. Defaults to True. + """ + assert_not_equal(first, second, msg=msg, snapshot=snapshot) + + @deco.keyword("查找所有") + def find_all(self, target: TargetType): + v = self.template(target) + return find_all(v) + + @deco.keyword("获取剪贴板") + def get_clipboard(self, wda_bundle_id: Optional[str] = None): + return get_clipboard() + + @deco.keyword("设置剪贴板") + def set_clipboard(self, content: str): + set_clipboard(content) + + @deco.keyword("双指手势") + def pinch( + self, + in_or_out: Literal["in", "out"], + center: Optional[Point] = None, + percent: float = 0.5, + ): + pinch(in_or_out=in_or_out, center=center, percent=percent) + + @deco.keyword("Home键") + def home(self): + home() + + @deco.keyword("唤醒") + def wake(self): + wake() diff --git a/robotframework_airtest/device/__init__.py b/robotframework_airtest/device/__init__.py index 9cd595e..b2107c3 100644 --- a/robotframework_airtest/device/__init__.py +++ b/robotframework_airtest/device/__init__.py @@ -1,8 +1,8 @@ -from typing import Tuple +from typing import Optional, Tuple, List from airtest.core.settings import Settings from robot.api import logger, deco from robot.libraries.BuiltIn import BuiltIn -from .connects import ConnectStrategyBase, factory +from .connects import factory # 坐标点类型声明 Point = Tuple[float, float] @@ -13,7 +13,10 @@ class DeviceLibrary: ROBOT_LIBRARY_FORMAT = "rsST" def __init__( - self, device_uri: str = "", pkg_name: str = None, auto_start_app: bool = False + self, + device_uri: str = "", + pkg_name: Optional[str] = None, + auto_start_app: bool = False, ): """初始化库是传入的参数将会作为连接设备关键字的默认参数用 @@ -26,7 +29,6 @@ def __init__( self.auto_start_app = auto_start_app or self.var("${auto_start_app}") == "True" self.device_uri = device_uri or self.var("${device_uri}") self.pkg_name = pkg_name or self.var("${pkg_name}") - self.conn: ConnectStrategyBase = None logger.console( "DeviceLibrary初始化 device_uri:{} pkg_name:{} auto_start_app:{}".format( self.device_uri, @@ -47,7 +49,7 @@ def var(self, name): def connect_device( self, _device_uri: str = "", - _pkg_name: str = None, + _pkg_name: Optional[str] = None, _auto_start_app: bool = False, ): """如果传入了 _device_uri参数那么就会在这一次连接设备覆盖掉DeviceLibrary初始化时的默认参数。 @@ -79,13 +81,13 @@ def disconnect_device(self): self.conn.disconnect() if self.conn else logger.console("设备并没有连接") @deco.keyword("开始录像") - def start_recording(self, output: str = None, *args, **kwargs): + def start_recording(self, output: Optional[str] = None, *args, **kwargs): save_path = self.conn.device.start_recording(output=output, *args, **kwargs) logger.console("设备开始录像") return save_path @deco.keyword("结束录像") - def stop_recording(self, output: str = None, *args, **kwargs): + def stop_recording(self, output: Optional[str] = None, *args, **kwargs): """结束录像 args和kwargs可以传递具体Device的start_recording的参数过去。 @@ -104,9 +106,9 @@ def is_disconnected(self): return not self.conn.is_connected @deco.keyword("截图") - def snapshot(self, filename: str = None, *args, **kwargs) -> bytes: + def snapshot(self, filename: Optional[str] = None, *args, **kwargs) -> bytes: data = self.conn.device.snapshot(filename=filename, *args, **kwargs) - return data + return data # type:ignore @deco.keyword("点击") def touch(self, pos: Point, **kwargs): @@ -148,8 +150,8 @@ def clear_app(self, package: str): self.conn.device.clear_app(package) @deco.keyword("列出安装的APP") - def list_app(self, **kwargs): - return self.conn.device.list_app(**kwargs) + def list_app(self, **kwargs) -> List[str]: + return self.conn.device.list_app(**kwargs) # type:ignore @deco.keyword("是否已安装APP") def is_app_installed(self, package: str, **kwargs): @@ -183,6 +185,6 @@ def get_ip_address(self): @deco.keyword("shell命令") def shell(self, *args, **kwargs) -> str: - return self.conn.device.shell(*args, **kwargs) + return self.conn.device.shell(*args, **kwargs) # type:ignore # endregion diff --git a/robotframework_airtest/poco/__init__.py b/robotframework_airtest/poco/__init__.py index 313e274..d93380f 100644 --- a/robotframework_airtest/poco/__init__.py +++ b/robotframework_airtest/poco/__init__.py @@ -1,5 +1,16 @@ from .std import StdPocoLibrary from .unity import UnityPocoLibrary +from .android import AndroidUiAutomationPocoLibrary +from .cocosjs import CocosJsPocoLibrary +from .ios import IOSPocoLibrary +from .ue4 import UE4PocoLibrary -__all__ = ["StdPocoLibrary", "UnityPocoLibrary"] +__all__ = [ + "StdPocoLibrary", + "UnityPocoLibrary", + "AndroidUiAutomationPocoLibrary", + "CocosJsPocoLibrary", + "IOSPocoLibrary", + "UE4PocoLibrary", +] diff --git a/tests/test_airtest/basic.png b/tests/test_airtest/basic.png new file mode 100644 index 0000000..4d7e810 Binary files /dev/null and b/tests/test_airtest/basic.png differ diff --git a/tests/test_airtest/btn_start.png b/tests/test_airtest/btn_start.png new file mode 100644 index 0000000..78bf3cf Binary files /dev/null and b/tests/test_airtest/btn_start.png differ diff --git a/tests/test_airtest/drag_drop.png b/tests/test_airtest/drag_drop.png new file mode 100644 index 0000000..1d78276 Binary files /dev/null and b/tests/test_airtest/drag_drop.png differ diff --git a/tests/test_airtest/shell.png b/tests/test_airtest/shell.png new file mode 100644 index 0000000..1e2f160 Binary files /dev/null and b/tests/test_airtest/shell.png differ diff --git a/tests/test_airtest/star.png b/tests/test_airtest/star.png new file mode 100644 index 0000000..02e4bef Binary files /dev/null and b/tests/test_airtest/star.png differ diff --git a/tests/test_airtest/test_airtest.robot b/tests/test_airtest/test_airtest.robot new file mode 100644 index 0000000..0948ff4 --- /dev/null +++ b/tests/test_airtest/test_airtest.robot @@ -0,0 +1,92 @@ +*** Settings *** +Library robotframework_airtest.AirtestLibrary WITH NAME airtest +Library robotframework_airtest.DeviceLibrary WITH NAME device +Library OperatingSystem + +Test Setup 初始化用例 +Test Teardown 清理用例 + + +*** Variables *** +${APK路径} tests/demo/com.netease.poco.u3d.tutorial.apk +${包名} com.NetEase + + +*** Test Cases *** +测试当前目录 + Log 当前目录:${CURDIR} level=CONSOLE + +测试点击 + airtest.点击 btn_start.png + +测试等待 + airtest.等待 btn_start.png 30s + +测试滑动 + airtest.点击 btn_start.png + airtest.点击 drag_drop.png + airtest.滑动 star.png shell.png + +测试查找所有 + airtest.点击 btn_start.png + airtest.点击 drag_drop.png + ${stars} airtest.查找所有 star.png + FOR ${element} IN @{stars} + Log ${element} level=CONSOLE + airtest.滑动 ${element} shell.png + END + airtest.必须不存在 star.png + +测试存在 + ${存在} airtest.存在 btn_start.png + Should Be True ${存在} + +测试输入文字 + airtest.点击 btn_start.png + airtest.点击 basic.png + airtest.点击 textbox.png + airtest.输入文字 hello + +测试按键事件 + airtest.按键事件 HOME + airtest.必须不存在 btn_start.png + +测试截图 + airtest.截图 screenshot.png + File Should Exist screenshot.png + Remove File screenshot.png + +睡眠 + airtest.睡眠 2s + +断言 + airtest.必须存在 btn_start.png + airtest.必须不存在 star.png + airtest.必须相等 btn_start.png btn_start.png + airtest.必须不相等 btn_start.png star.png + +测试剪贴板 + airtest.设置剪贴板 hello + ${结果} airtest.获取剪贴板 + Should Be Equal As Strings ${结果} hello + +测试双指手势 + airtest.双指手势 in + +测试Home键 + airtest.Home键 + +测试唤醒 + airtest.唤醒 + + +*** Keywords *** +初始化用例 + 连接设备 android:/// + ${已安装} 是否已安装APP ${包名} + IF ${已安装} == ${False} 安装APP ${APK路径} + 启动APP ${包名} + Sleep 3s + +清理用例 + 停止APP ${包名} diff --git a/tests/test_airtest/textbox.png b/tests/test_airtest/textbox.png new file mode 100644 index 0000000..eea8905 Binary files /dev/null and b/tests/test_airtest/textbox.png differ diff --git a/tests/test_device_library_android.robot b/tests/test_device_library_android.robot index e5e1ef6..8d924ba 100644 --- a/tests/test_device_library_android.robot +++ b/tests/test_device_library_android.robot @@ -1,6 +1,6 @@ *** Settings *** Library robotframework_airtest.device.DeviceLibrary -Library ../.venv/lib/site-packages/robot/libraries/OperatingSystem.py +Library OperatingSystem Test Setup 初始化用例 Test Teardown 断开设备