10

Airtest 和 Poco 的 API 总结

 3 years ago
source link: https://cuiqingcai.com/9529.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

扫码或搜索: 进击的Coder

发送

即可 立即永久 解锁本站全部文章

zM73Abu.jpg!web

Airtest

首先需要安装 Airtest,使用 pip3 即可:

pip3 install airtest

初始化 device

如果设备没有被初始化的话会进行初始化,并把初始化的设备作为当前设备。

用法如下:

def init_device(platform="Android", uuid=None, **kwargs):
    """
    Initialize device if not yet, and set as current device.
 
    :param platform: Android, IOS or Windows
    :param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS
    :param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android
    :return: device instance
    """

示例如下:

device = init_device('Android')
print(device)

运行结果如下:

<airtest.core.android.android.Android object at 0x1018f3a58>

可以发现它返回的是一个 Android 对象。

这个 Android 对象实际上属于 airtest.core.android 这个包,继承自 airtest.core.device.Device 这个类,与之并列的还有 airtest.core.ios.ios.IOSairtest.core.linux.linux.Linuxairtest.core.win.win.Windows 等。这些都有一些针对 Device 操作的 API,下面我们以 airtest.core.android.android.Android 为例来总结一下。

  • get_default_device:获取默认 device
  • uuid:获取当前 Device 的 UUID
  • list_app:列举所有 App
  • path_app:打印输出某个 App 的完整路径
  • check_app:检查某个 App 是否在当前设备上
  • start_app:启动某个 App
  • start_app_timing:启动某个 App,然后计算时间
  • stop_app:停止某个 App
  • clear_app:清空某个 App 的全部数据
  • install_app:安装某个 App
  • install_multiple_app:安装多个 App
  • uninstall_app:卸载某个 App
  • snapshot:屏幕截图
  • shell:获取 Adb Shell 执行的结果
  • keyevent:执行键盘操作
  • wake:唤醒当前设备
  • home:点击 HOME 键
  • text:向设备输入内容
  • touch:点击屏幕某处的位置
  • double_click:双击屏幕某处的位置
  • swipe:滑动屏幕,由一点到另外一点
  • pinch:手指捏和操作
  • logcat:日志记录操作
  • getprop:获取某个特定属性的值
  • get_ip_address:获取 IP 地址
  • get_top_activity:获取当前 Activity
  • get_top_activity_name_and_pid:获取当前 Activity 的名称和进程号
  • get_top_activity_name:获取当前 Activity 的名称
  • is_keyboard_shown:判断当前键盘是否出现了
  • is_locked:设备是否锁定了
  • unlock:解锁设备
  • display_info:获取当前显示信息,如屏幕宽高等
  • get_display_info:同 display_info
  • get_current_resolution:获取当前设备分辨率
  • get_render_resolution:获取当前渲染分辨率
  • start_recording:开始录制
  • stop_recording:结束录制
  • adjust_all_screen:调整屏幕适配分辨率

了解了上面的方法之后,我们可以用一个实例来感受下它们的用法:

from airtest.core.android import Android
from airtest.core.api import *
import logging
 
logging.getLogger("airtest").setLevel(logging.WARNING)
 
device: Android = init_device('Android')
is_locked = device.is_locked()
print(f'is_locked: {is_locked}')
 
if is_locked:
    device.unlock()
 
device.wake()
 
app_list = device.list_app()
print(f'app list {app_list}')
 
uuid = device.uuid
print(f'uuid {uuid}')
 
display_info = device.get_display_info()
print(f'display info {display_info}')
 
resolution = device.get_render_resolution()
print(f'resolution {resolution}')
 
ip_address = device.get_ip_address()
print(f'ip address {ip_address}')
 
top_activity = device.get_top_activity()
print(f'top activity {top_activity}')
 
is_keyboard_shown = device.is_keyboard_shown()
print(f'is keyboard shown {is_keyboard_shown}')

这里我们调用了设备的一些操作方法,获取了一些基本状态,运行结果如下:

is_locked: False
app list ['com.kimcy929.screenrecorder', 'com.android.providers.telephony', 'io.appium.settings', 'com.android.providers.calendar', 'com.android.providers.media', 'com.goldze.mvvmhabit', 'com.android.wallpapercropper', 'com.android.documentsui', 'com.android.galaxy4', 'com.android.externalstorage', 'com.android.htmlviewer', 'com.android.quicksearchbox', 'com.android.mms.service', 'com.android.providers.downloads', 'mark.qrcode', ..., 'com.google.android.play.games', 'io.kkzs', 'tv.danmaku.bili', 'com.android.captiveportallogin']
uuid emulator-5554
display info {'id': 0, 'width': 1080, 'height': 1920, 'xdpi': 320.0, 'ydpi': 320.0, 'size': 6.88, 'density': 2.0, 'fps': 60.0, 'secure': True, 'rotation': 0, 'orientation': 0.0, 'physical_width': 1080, 'physical_height': 1920}
resolution (0.0, 0.0, 1080.0, 1920.0)
ip address 10.0.2.15
top activity ('com.microsoft.launcher.dev', 'com.microsoft.launcher.Launcher', '16040')
is keyboard shown False

连接 device

连接 device 需要传入设备的 uri,格式类似 android://adbhost:adbport/serialno?param=value&param2=value2 ,使用方法如下:

def connect_device(uri):
    """
    Initialize device with uri, and set as current device.
 
    :param uri: an URI where to connect to device, e.g. `android://adbhost:adbport/serialno?param=value&param2=value2`
    :return: device instance
    :Example:
        * ``android:///`` # local adb device using default params
        * ``android://adbhost:adbport/1234566?cap_method=javacap&touch_method=adb``  # remote device using custom params
        * ``windows:///`` # local Windows application
        * ``ios:///`` # iOS device
    """

示例如下:

from airtest.core.android import Android
from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(device)

运行结果如下:

<airtest.core.android.android.Android object at 0x110246940>

其实返回结果和 init_device 是一样的,最后 connect_device 方法就是调用了 init_device 方法。

获取当前 device

就是直接调用 device 方法,定义如下:

def device():
    """
    Return the current active device.
 
    :return: current device instance
    """
    return G.DEVICE

获取所有 device

在 airtest 中有一个全局变量 G,获取所有 device 的方法如下:

from airtest.core.android import Android
from airtest.core.api import *
 
print(G.DEVICE_LIST)
uri = 'Android://127.0.0.1:5037/emulator-5554'
device: Android = connect_device(uri)
print(G.DEVICE_LIST)

运行结果如下:

[]
[<airtest.core.android.android.Android object at 0x10ba03978>]

这里需要注意的是,在最开始没有调用 connect_device 方法之前,DEVICE_LIST 是空的,在调用之后 DEVICE_LIST 会自动添加已经连接的 device,DEVICE_LIST 就是已经连接的 device 列表

切换 device

我们可以使用 set_current 方法切换当前连接的 device,传入的是 index,定义如下:

def set_current(idx):
    """
    Set current active device.
 
    :param idx: uuid or index of initialized device instance
    :raise IndexError: raised when device idx is not found
    :return: None
    :platforms: Android, iOS, Windows
    """

这个方法没有返回值,调用 set_current 方法切换 device 之后,再调用 device 方法就可以获取当前 device 对象了。

执行命令行

可以使用 shell 方法传入 cmd 来执行命令行,定义如下:

@logwrap
def shell(cmd):
    """
    Start remote shell in the target device and execute the command
 
    :param cmd: command to be run on device, e.g. "ls /data/local/tmp"
    :return: the output of the shell cmd
    :platforms: Android
    """
    return G.DEVICE.shell(cmd)

直接调用 adb 命令就好了,例如获取内存信息就可以使用如下命令:

from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
 
result = shell('cat /proc/meminfo')
print(result)

运行结果如下:

MemTotal:        3627908 kB
MemFree:         2655560 kB
MemAvailable:    2725928 kB
Buffers:            3496 kB
Cached:           147472 kB
SwapCached:            0 kB
Active:           744592 kB
Inactive:         126332 kB
Active(anon):     723292 kB
Inactive(anon):    16344 kB
Active(file):      21300 kB
Inactive(file):   109988 kB
Unevictable:           0 kB
Mlocked:               0 kB
HighTotal:       2760648 kB
HighFree:        2073440 kB
LowTotal:         867260 kB
LowFree:          582120 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:        720100 kB
Mapped:           127720 kB
Shmem:             19428 kB
Slab:              76196 kB
SReclaimable:       7392 kB
SUnreclaim:        68804 kB
KernelStack:        7896 kB
PageTables:         8544 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     1813952 kB
Committed_AS:   21521776 kB
VmallocTotal:     122880 kB
VmallocUsed:       38876 kB
VmallocChunk:      15068 kB
DirectMap4k:       16376 kB
DirectMap4M:      892928 kB

启动和停止 App

启动和停止 App 直接传入包名即可,其实它们就是调用的 device 的 start_app 和 stop_app 方法,定义如下:

@logwrap
def start_app(package, activity=None):
    """
    Start the target application on device
 
    :param package: name of the package to be started, e.g. "com.netease.my"
    :param activity: the activity to start, default is None which means the main activity
    :return: None
    :platforms: Android, iOS
    """
    G.DEVICE.start_app(package, activity)
 
@logwrap
def stop_app(package):
    """
    Stop the target application on device
 
    :param package: name of the package to stop, see also `start_app`
    :return: None
    :platforms: Android, iOS
    """
    G.DEVICE.stop_app(package)

用法示例如下:

from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
 
package = 'com.tencent.mm'
start_app(package)
sleep(10)
stop_app(package)

这里我指定了微信的包名,然后调用 start_app 启动了微信,然后等待了 10 秒,然后调用了 stop_app 停止了微信。

安装和卸载

安装和卸载也是一样,也是调用了 device 的 install 和 uninstall 方法,定义如下:

@logwrap
def install(filepath, **kwargs):
    """
    Install application on device
 
    :param filepath: the path to file to be installed on target device
    :param kwargs: platform specific `kwargs`, please refer to corresponding docs
    :return: None
    :platforms: Android
    """
    return G.DEVICE.install_app(filepath, **kwargs)
 
@logwrap
def uninstall(package):
    """
    Uninstall application on device
 
    :param package: name of the package, see also `start_app`
    :return: None
    :platforms: Android
    """
    return G.DEVICE.uninstall_app(package)

截图

截图使用 snapshot 即可完成,可以设定存储的文件名称,图片质量等。

定义如下:

def snapshot(filename=None, msg="", quality=ST.SNAPSHOT_QUALITY):
    """
    Take the screenshot of the target device and save it to the file.
 
    :param filename: name of the file where to save the screenshot. If the relative path is provided, the default
                     location is ``ST.LOG_DIR``
    :param msg: short description for screenshot, it will be recorded in the report
    :param quality: The image quality, integer in range [1, 99]
    :return: absolute path of the screenshot
    :platforms: Android, iOS, Windows
    """

示例如下:

from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
 
package = 'com.tencent.mm'
start_app(package)
sleep(3)
snapshot('weixin.png', quality=30)

运行之后在当前目录会生成一个 weixin.png 的图片,如图所示:

32Yze2j.png!web

唤醒和首页

唤醒和回到首页分别也是调用了 device 的 wake 和 home 方法,定义如下:

@logwrap
def wake():
    """
    Wake up and unlock the target device
 
    :return: None
    :platforms: Android
 
    .. note:: Might not work on some models
    """
    G.DEVICE.wake()
 
@logwrap
def home():
    """
    Return to the home screen of the target device.
 
    :return: None
    :platforms: Android, iOS
    """
    G.DEVICE.home()

直接调用即可。

点击屏幕

点击屏幕是 touch 方法,可以传入一张图或者绝对位置,同时可以指定点击次数,定义如下:

@logwrap
def touch(v, times=1, **kwargs):
    """
    Perform the touch action on the device screen
 
    :param v: target to touch, either a Template instance or absolute coordinates (x, y)
    :param times: how many touches to be performed
    :param kwargs: platform specific `kwargs`, please refer to corresponding docs
    :return: finial position to be clicked
    :platforms: Android, Windows, iOS
    """

例如我现在的手机屏幕是这样子:

AbmUnmq.png!web

这里我截图下来一张图片,如图所示:

NzeaAnF.png!web

然后我们把这个图片声明成一个 Template 传入,示例如下:

from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
touch(Template('tpl.png'))

启动之后它就会识别出这张图片的位置,然后点击。

或者我们可以指定点击的绝对位置,示例如下:

from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
touch((400, 600), times=2)

另外上述的 touch 方法还可以完全等同于 click 方法。

如果要双击的话,还可以使用调用 double_click 方法,传入参数也可以是 Template 或者绝对位置。

滑动

滑动可以使用 swipe 方法,可以传入起始和终止位置,两个位置都可以传入绝对位置或者 Template,定义如下:

@logwrap
def swipe(v1, v2=None, vector=None, **kwargs):
    """
    Perform the swipe action on the device screen.
 
    There are two ways of assigning the parameters
        * ``swipe(v1, v2=Template(...))``   # swipe from v1 to v2
        * ``swipe(v1, vector=(x, y))``      # swipe starts at v1 and moves along the vector.
 
    :param v1: the start point of swipe,
               either a Template instance or absolute coordinates (x, y)
    :param v2: the end point of swipe,
               either a Template instance or absolute coordinates (x, y)
    :param vector: a vector coordinates of swipe action, either absolute coordinates (x, y) or percentage of
                   screen e.g.(0.5, 0.5)
    :param **kwargs: platform specific `kwargs`, please refer to corresponding docs
    :raise Exception: general exception when not enough parameters to perform swap action have been provided
    :return: Origin position and target position
    :platforms: Android, Windows, iOS
    """

比如这里我们可以定义手指向右滑动:

from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
swipe((200, 300), (900, 300))

放大缩小

放大缩小是使用的 pinch 方法,可以指定放大还是缩小,同时还可以指定中心位置点和放大缩小的比率。

定义如下:

@logwrap
def pinch(in_or_out='in', center=None, percent=0.5):
    """
    Perform the pinch action on the device screen
 
    :param in_or_out: pinch in or pinch out, enum in ["in", "out"]
    :param center: center of pinch action, default as None which is the center of the screen
    :param percent: percentage of the screen of pinch action, default is 0.5
    :return: None
    :platforms: Android
    """

示例如下:

from airtest.core.api import *
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
pinch(in_or_out='out', center=(300, 300), percent=0.4)

键盘事件

可以使用 keyevent 来输入某个键,例如 home、back 等等,keyevent 的定义如下:

def keyevent(keyname, **kwargs):
    """
    Perform key event on the device
 
    :param keyname: platform specific key name
    :param **kwargs: platform specific `kwargs`, please refer to corresponding docs
    :return: None
    :platforms: Android, Windows, iOS
    """

示例如下:

keyevent("HOME")

这样就表示按了 HOME 键。

输入内容

输入内容需要使用 text 方法,当然前提是这个 widget 需要是 active 状态,text 的定义如下:

@logwrap
def text(text, enter=True, **kwargs):
    """
    Input text on the target device. Text input widget must be active first.
 
    :param text: text to input, unicode is supported
    :param enter: input `Enter` keyevent after text input, default is True
    :return: None
    :platforms: Android, Windows, iOS
    """

等待和判断

可以使用 wait 方法等待某个内容加载出来,需要传入的是 Template,定义如下:

@logwrap
def wait(v, timeout=None, interval=0.5, intervalfunc=None):
    """
    Wait to match the Template on the device screen
 
    :param v: target object to wait for, Template instance
    :param timeout: time interval to wait for the match, default is None which is ``ST.FIND_TIMEOUT``
    :param interval: time interval in seconds to attempt to find a match
    :param intervalfunc: called after each unsuccessful attempt to find the corresponding match
    :raise TargetNotFoundError: raised if target is not found after the time limit expired
    :return: coordinates of the matched target
    :platforms: Android, Windows, iOS
    """

同时也使用 exists 方法判断某个内容是否存在,定义如下:

@logwrap
def exists(v):
    """
    Check whether given target exists on device screen
 
    :param v: target to be checked
    :return: False if target is not found, otherwise returns the coordinates of the target
    :platforms: Android, Windows, iOS
    """

断言

另外 Airtest 还提供了几个断言语句来判断结果是否存在或者相同,定义如下:

@logwrap
def assert_exists(v, msg=""):
    """
    Assert target exists on device screen
 
    :param v: target to be checked
    :param msg: short description of assertion, it will be recorded in the report
    :raise AssertionError: if assertion fails
    :return: coordinates of the target
    :platforms: Android, Windows, iOS
    """
    try:
        pos = loop_find(v, timeout=ST.FIND_TIMEOUT, threshold=ST.THRESHOLD_STRICT)
        return pos
    except TargetNotFoundError:
        raise AssertionError("%s does not exist in screen, message: %s" % (v, msg))
 
@logwrap
def assert_not_exists(v, msg=""):
    """
    Assert target does not exist on device screen
 
    :param v: target to be checked
    :param msg: short description of assertion, it will be recorded in the report
    :raise AssertionError: if assertion fails
    :return: None.
    :platforms: Android, Windows, iOS
    """
    try:
        pos = loop_find(v, timeout=ST.FIND_TIMEOUT_TMP)
        raise AssertionError("%s exists unexpectedly at pos: %s, message: %s" % (v, pos, msg))
    except TargetNotFoundError:
        pass
 
@logwrap
def assert_equal(first, second, msg=""):
    """
    Assert two values are equal
 
    :param first: first value
    :param second: second value
    :param msg: short description of assertion, it will be recorded in the report
    :raise AssertionError: if assertion fails
    :return: None
    :platforms: Android, Windows, iOS
    """
    if first != second:
        raise AssertionError("%s and %s are not equal, message: %s" % (first, second, msg))
 
@logwrap
def assert_not_equal(first, second, msg=""):
    """
    Assert two values are not equal
 
    :param first: first value
    :param second: second value
    :param msg: short description of assertion, it will be recorded in the report
    :raise AssertionError: if assertion
    :return: None
    :platforms: Android, Windows, iOS
    """
    if first == second:
        raise AssertionError("%s and %s are equal, message: %s" % (first, second, msg))

这几个断言比较常用的就是 assert_exists 和 assert_not_exists 判断某个目标是否存在于屏幕上,同时还可以传入 msg,它可以被记录到 report 里面。

以上就是 Airtest 的 API 的用法,它提供了一些便捷的方法封装,同时还对接了图像识别等技术。

但 Airtest 也有一定的局限性,比如不能根据 DOM 树来选择对应的节点,依靠图像识别也有一定的不精确之处,所以还需要另外一个库 —— Poco。

Poco

利用 Poco 我们可以支持 DOM 选择,例如编写 XPath 等来定位某一个节点。

首先需要安装 Poco,使用 pip3 即可:

pip3 install pocoui

安装好了之后我们便可以使用它来选择一些节点了,示例如下:

from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
 
poco = AndroidUiautomationPoco()
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
poco(text='Weather').click()

比如这里我们就声明了 AndroidUiautomationPoco 这个 Poco 对象,然后调用了 poco 传入一些选择器,选中之后执行 click 方法。

这个选择器非常强大,可以传入 name 和各个属性值,具体的使用方法见: https://poco.readthedocs.io/en/latest/source/poco.pocofw.html

from airtest.core.api import *
from poco.drivers.android.uiautomation import AndroidUiautomationPoco
from poco.proxy import UIObjectProxy
 
poco = AndroidUiautomationPoco()
 
uri = 'Android://127.0.0.1:5037/emulator-5554'
connect_device(uri)
home()
object: UIObjectProxy = poco("com.microsoft.launcher.dev:id/workspace")
print(object)

poco 返回的是 UIObjectProxy 对象,它提供了其他的操作 API,例如选取子节点,兄弟节点,父节点等等,同时可以调用各个操作方法,如 click、pinch、scroll 等等。

具体的操作文档可以参见: https://poco.readthedocs.io/en/latest/source/poco.proxy.html

下面简单总结:

  • attr:获取节点属性
  • child:获取子节点
  • children:获取所有子节点
  • click:点击
  • double_click:双击
  • drag_to:将某个节点拖拽到另一个节点
  • exists:某个节点是否存在
  • focus:获得焦点
  • get_bounds:获取边界
  • get_name:获取节点名
  • get_position:获取节点位置
  • get_size:获取节点大小
  • get_text:获取文本内容
  • long_click:长按
  • offspring:选出包含直接子代的后代
  • parent:父节点
  • pinch:缩放
  • scroll:滑动
  • set_text:设置文字
  • sibling:兄弟节点
  • swipe:滑动
  • wait:等待
  • wait_for_appearance:等待某个节点出现
  • wait_for_disappearance:等待某个节点消失

以上的这些方法混用的话就可以执行各种节点的选择和相应的操作。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK