上一篇 - Macaca 是如何封装 ADB 的

Macaca App Inspector 原理解析 https://testerhome.com/topics/8896

如下内容已过期。


最近很多 Macaca 的用户都会问关于如何查找 Native 界面元素,如何找到映射的类名,如何定位 Webview 或者传统网页的 tag 元素,今天花些时间把原理、经验等做个总结。

首先介绍 Android 平台的做法,AndroidSDK 提供了界面查看器 uiautomatorviewer,我们一探究竟。

UI Automator Viewer

先说几句

熟悉 Android 平台开发和测试的同学应该都清楚 uiautomatorviewer 的位置,首先需要通过模拟器启动一个设备,或者连接真机。

# 如下即可启动 uiautomatorviewer 的可执行文件
$ $ANDROID_HOME/tools/uiautomatorviewer

我们就可以看到初始界面了

实现原理

同级目录下的 lib 目录下可以找到 uiautomatorviewer.jar,可以通过反编译(也可以直接找到源代码)查看下里面的实现,其实很简单。

大体原理如下:

初始化 ADB 调试协议

Dump 设备当前的 HierarchyXml,可以发现它将 UI 的描述 xml 存放在系统的临时目录

# 默认大家都有 node 环境,我们来输出一下临时目录
$ node -e "console.log(require('os').tmpdir())"

> /var/folders/qf/gtyrygk530ndzp6crby9phv40000gn/T

可以看到生成的临时目录下有当前的截图图片,和一个 .uix 文件。

.uix 就是当前界面的 xml 描述,可以看到上面有基本的 bounds, package, class 等字段。

加载、解析 xml 树,并将相应结构渲染到截图上方,以此来相应用户的鼠标操作

相应用户操作也比较处理,常规的 2d 渲染,加事件捕捉即可,可以用我之前写的一个引擎来感受下这个操作。

在线 demo 中的实现是一个 canvas 元素,其实可以看出,uiautomatorviewer 的实现也是个 canvas:

渲染和事件捕获的原理说差不多了,接下来就容易理解用法和操作了。

用法

我们把鼠标移到想要查看的元素上方,右侧就可以看到类名了,非常简单的操作,可以发现右侧的展示树已经把类名的前缀都去掉了:

// com.android.uiautomator.tree.UINode

private void updateDisplayName() {
    String className = (String)this.mAttributes.get("class");
    if(className != null) {
        String text = (String)this.mAttributes.get("text");
        if(text != null) {
            String contentDescription = (String)this.mAttributes.get("content-desc");
            if(contentDescription != null) {
                String index = (String)this.mAttributes.get("index");
                if(index != null) {
                    String bounds = (String)this.mAttributes.get("bounds");
                    if(bounds != null) {
                        className = className.replace("android.widget.", "");
                        className = className.replace("android.view.", "");
                        StringBuilder builder = new StringBuilder();
                        builder.append('(');
                        builder.append(index);
                        builder.append(") ");
                        builder.append(className);
                        if(!text.isEmpty()) {
                            builder.append(':');
                            builder.append(text);
                        }

                        if(!contentDescription.isEmpty()) {
                            builder.append(" {");
                            builder.append(contentDescription);
                            builder.append('}');
                        }

                        builder.append(' ');
                        builder.append(bounds);
                        this.mDisplayName = builder.toString();
                    }
                }
            }
        }
    }
}

写一个测试用例

通过 uiautomatorviewer,我们可以用如下的代码拿到一个输入框控件:

elementByNameelementByClassName 是最常用的两个获取元素的 API,我们尽量避免在 Android 上使用 Xpath,而是使用 elementByClassName 来做到。

// 这里是一个最简单的登录界面的登录封装

module.exports = function(username, password) {
  return this
    .waitForElementsByClassName('android.widget.EditText', {}, 120000)
    .then(function(els) {
      return els[0];
    })
    .sendKeys(username)
    .sleep(1000)
    .elementsByClassName('android.widget.EditText')
    .then(function(els) {
      return els[1];
    })
    .sendKeys(password)
    .sleep(1000)
    .waitForElementByName('Login')
    .click()
    .sleep(5000);
};

这里对应的 layout 代码如下:

<RelativeLayout
    android:id="@+id/topline"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="20dp"
    android:layout_marginRight="20dp"
    android:layout_marginTop="120dp"
    android:background="@drawable/login_layout_bg"
    android:orientation="horizontal"
    android:weightSum="1">

    <EditText
        android:id="@+id/mobileNoEditText"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_marginLeft="2dp"
        android:layout_weight="0.52"
        android:background="@null"
        android:digits="0123456789"
        android:hint="@string/username_tips"
        android:inputType="textNoSuggestions"
        android:orientation="horizontal"
        android:phoneNumber="true"
        android:singleLine="true"
        android:textColor="@color/black"
        android:textSize="15dp" />

</RelativeLayout>

更理想的实现

期待有同学使用 Electron,做一个更轻的实现,直接在浏览器浏览和相应用户操作,同时使用 Inspector 查看映射的结构,使用体验就更佳了。

不过目前官方的轮子已经足够我们用了。

Android APP 中的 Webview

通过打开 Chrome 浏览器调试界面就可以找到 Webview 的页面了。

chrome://inspect/#devices

可以看到通过 Webkit 调试协议查看到的页面,如果你测试的 App 使用了自己修改过的内核或者不是 Webkit 内核,那将无法查看。

Android 内置的浏览器也支持这样操作,可见每一个连接都是通过包名区分的,有兴趣的可以看 macaca-android 对这里的实现:

Android.prototype.initChromeDriver = function() {
  return new Promise((resolve, reject) => {
    this.chromedriver = new ChromeDriver();
    this.chromedriver.on(ChromeDriver.EVENT_READY, data => {
      logger.info(`chromedriver ready with: ${JSON.stringify(data)}`);
      resolve('');
    });
    this.chromedriver.start({
      chromeOptions: {
        androidPackage: this.apkInfo.package,
        androidUseRunningApp: true,
        androidDeviceSerial: this.udid
      }
    });
  });
};

通过 Inspector 调试器就可以查找和定位元素。


Inspector 如何使用

如果你是前端同学,可以绕过这里了,哈哈。

通过右键 Copy -> Copy selector 或者 Copy -> Copy XPath 可以拷贝这两种选择器的字符参数,然后调用 Macaca 提供的 API 方法即可。

例如示例 macaca-test-sample 中的:


it('#1 should works with macaca', function() {
  return driver
    .elementById('kw')                           // 通过元素 id 获取
    .sendKeys('macaca')
    .sleep(3000)
    .elementById('su')
    .click()
    .sleep(5000)
    .source()
    .then(function(html) {
      html.should.containEql('macaca');
    })
    .hasElementByCss('#head > div.head_wrapper') // 通过元素 selector 获取
    .then(function(hasHeadWrapper) {
      hasHeadWrapper.should.be.true();
    })
    .elementByXPathOrNull('//*[@id="kw"]')       // 通过元素 XPath 获取
    .sendKeys(' elementByXPath')
    .sleep(3000)
    .elementById('su')
    .click()
    .sleep(5000)
    .takeScreenshot();
});

iOS 查找元素

OSX 中提供的 Xcode -> Instruments -> Automation 支持 Javascript 语法获取元素信息,如:

UIATarget.localTarget().logElementTree();

不过这样做实在麻烦,而且要先录制,速度很慢,需要的系统资源很高。

iOS 平台我们倾向使用 Accessibility Inspector,它是 OSX 提供的无障碍系列工具,更多关于 Authoring Tool Accessibility Guidelines

通过如下方式启动 Accessibility Inspector:

Xcode -> Open Developer Tool -> Accessibility Inspector

这里是 Accessibility Inspector 的配置文件:

~/Library/Preferences/com.apple.AccessibilityInspector.plist

可以看到已经找到了元素名 AXTextField,对应的控件就是 UIATextField,所以我们的登陆功能可以这么封装:

module.exports = function(username, password) {
  return this
    .waitForElementByXPath('//UIATextField[1]')
    .sendKeys(username)
    .waitForElementByXPath('//UIASecureTextField[1]')
    .sendKeys(password)
    .sleep(1000)
    .sendKeys('\n')
    .waitForElementByName('Login')
    .click()
    .sleep(5000);
};

我通过反射的方式,把 iOS 的 runtime 类关系都打印并绘制了出来,可以通过继承树图来更多的熟悉 iOS 中的 UI 控件类。

iOS 中的 Webview

Macaca 对 iOS 中的 Webview 实现依赖于 ios_webkit_debug_proxy,当你在运行 Macaca 或者通过单步调试方式调试用例的时候,访问 9222 端口即可看到当前打开了哪些网页,调试请通过 Inpsector,这里不赘述。

相关文章:Macaca 测试用例 - 单步调试

下一篇 - macaca-electron 模块的独立使用


↙↙↙阅读原文可查看相关链接,并与作者交流