如下内容已过期。
最近很多 Macaca 的用户都会问关于如何查找 Native
界面元素,如何找到映射的类名,如何定位 Webview
或者传统网页的 tag
元素,今天花些时间把原理、经验等做个总结。
首先介绍 Android
平台的做法,Android
的 SDK
提供了界面查看器 uiautomatorviewer
,我们一探究竟。
熟悉 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
,我们可以用如下的代码拿到一个输入框控件:
elementByName
和 elementByClassName
是最常用的两个获取元素的 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 查看映射的结构,使用体验就更佳了。
不过目前官方的轮子已经足够我们用了。
通过打开 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 调试器就可以查找和定位元素。
如果你是前端同学,可以绕过这里了,哈哈。
通过右键 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();
});
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 控件类。
Macaca
对 iOS 中的 Webview
实现依赖于 ios_webkit_debug_proxy
,当你在运行 Macaca
或者通过单步调试方式调试用例的时候,访问 9222
端口即可看到当前打开了哪些网页,调试请通过 Inpsector
,这里不赘述。
相关文章:Macaca 测试用例 - 单步调试