记得好久以前分析了robotium对native控件的支持,最近在研究webview的测试,所以继续看下robotium对webview部分的支持。那么我们稍微复习一下上次分析到结论:
1.利用decoderView获取控件树的方式完成对控件的查找
2.最后调用的instrumentation的inject注入event事件完成对控件的操作
3.利用junit的assert类或者hamcreast框架进行结果判断。
上一篇详情可以查看:Robotium整体源码浅析
(写的没这次好,毕竟水平是越来越高的是不,看有没有时间重新分析下 :) )
照着这个思路,今天一起来学习下robotium中webview的部分
源码地址:robotiumgit主页
RobotiumWeb.js:
这个js文件是要注入到webview里面的,定义了一堆方法,用来查找元素和操作元素
RobotiumWebClient.java:
继承WebChromeClient类,主要是为了重写onJsPrompt方法,把查找的元素信息通过回调给到robotium,然后进行封装
WebElement.java:
把查找到的元素信息封装成实体类,这个就是那个类了
WebElementCreator.java:
看名字就知道是创建webelement的,把解析过来的element信息string,包装成WebElement对象
WebUtils
工具类,入口类,包含执行查找,js注入的函数,文本注入等,需要重点关注的入口
最后,提上和webview相关的api
webview支持的原理离不开上面这个api了,这几个api排除重载的方法后,可以分成下面一下几个:
- 查找所有或者当前屏幕的的webElement对象,返回列表
- 查找一个webElement对象,返回这个element对象
- 注入文字
- 点击Element
- 等待element元素出现
首先是solo.getCurrentWebElements,这个方法和getWebElements一致,只是是否可见作为一个形参传入,所以直接看solo.getWebElements这个方法。
public ArrayList getWebElements(boolean onlySufficientlyVisible){
boolean javaScriptWasExecuted = executeJavaScriptFunction("allWebElements();");
return getWebElements(javaScriptWasExecuted, onlySufficientlyVisible);
}
那么跟踪这个方法,最终调用的是executeJavaScriptFunction这个方法,executeJavaScriptFunction就是执行js注入的方法:
private boolean executeJavaScriptFunction(final String function) {
List webViews = viewFetcher.getCurrentViews(WebView.class, true);
final WebView webView = viewFetcher.getFreshestView((ArrayList) webViews);
if(webView == null) {
return false;
}
final String javaScript = setWebFrame(prepareForStartOfJavascriptExecution(webViews));
inst.runOnMainSync(new Runnable() {
public void run() {
if(webView != null){
webView.loadUrl("javascript:" + javaScript + function);
}
}
});
return true;
}
那么上面可以看到主要做了有下面3个事情:
所以,重点就变成这个注入的js脚本是什么,直接打开RobotiumWeb.js
那么可以看到这里定义了一堆方法,看这些名称,看起来功能实现不就是和一开始的api相呼应么,看到这里也就明白了,执行js注入查找,本质上是通过dom里面的api查找的,我们打开allWebElements这个js函数看下:
function allWebElements() {
for (var key in document.all){
try{
promptElement(document.all[key]);
}catch(ignored){}
}
finished();
}
嗯,到这里终于明确了:
prompt(id + ';,' + text + ';,' + name + ";," + className + ";," + tagName + ";," + rect.left + ';,' + rect.top + ';,' + rect.width + ';,' + rect.height + ';,' + attributes);
function finished(){
prompt('robotium-finished');
}
好了,有了上面的分析,我们很明确下一部就是要把prompt发出来的消息接收并封装成WebElement对象
在executeJavaScriptFunction方法中我们有一步是初始化WebChromeView,这一步使用的是robotium自定义的robotiumWebCLient对象,里面就有重写的onJsPrompt方法。
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) {
if(message != null && (message.contains(";,") || message.contains("robotium-finished"))){
if(message.equals("robotium-finished")){
webElementCreator.setFinished(true);
}
else{
webElementCreator.createWebElementAndAddInList(message, view);
}
r.confirm();
return true;
}
else {
if(originalWebChromeClient != null) {
return originalWebChromeClient.onJsPrompt(view, url, message, defaultValue, r);
}
return true;
}
}
这个方法最后调用了WebElementCreator.createWebElementAndAddInList方法:
最后,好像好剩下一个方法:getCurrentWebElements
这个方法和getWebElements相比,在返回element列表过程中多加了一步过滤,做的工作是把Element的x,y坐标和webview坐标做比较,把坐标在webview之外的过滤掉,也就是返回的是当前展示的内容啦。
好的,查找核心的东西就这么多了,然后上面是查找全部或者一组,那么查找单个也是同理的,在js文件中封装id、xpath、cssSelector等方法,这些方式刚好都是调用document相关的api,写web端的同学相信会非常熟悉这些定位方式。
robotium对webview的点击支持两种方式,一种和native一样,点击坐标点;另一种则是js脚本的方式。
坐标点:
在RobotiumWeb.js的promptElement方法中,会调用:
”’
var rect = element.getBoundingClientRect();
”’
这就可以得到元素的x,y坐标啦,然后就可以调用solo.clickOnScreen方法点击了
js脚本:
”’
function clickElement(element){
var e = document.createEvent(‘MouseEvents’);
e.initMouseEvent(‘click’, true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(e);
}
”’
需要注意的是,solo其实并没有提供入口给我们使用第二种方法点击,如果需要使用js方式点击,需要在初始化的时候配置config对象,默认都是会去查找然后转成坐标点进行操作的。
这是solo提供关于text的api,所以提供了两种方式注入文本
enterTextInWebElement
typeTextInWebElement
有啥区别呢,对于enterxxx来说:
是调用类似setText的方法设置的,在robotiumWeb.js可以看到是直接设置By定位到的元素的value设置的,而typeTextInWebElement方式则是:
”’
public void typeTextInWebElement(By by, String text, int match){
if(config.commandLogging){
Log.d(config.commandLoggingTag, “typeTextInWebElement(“+by+”, \”“+text+”\”, “+match+”)”);
}
clicker.clickOnWebElement(by, match, true, false);
dialogUtils.hideSoftKeyboard(null, true, true);
instrumentation.sendStringSync(text);
}
”’
点击–隐藏弹出的输入法–调用instrumentation.sendStringSync方法,这个方法是拆分组个字符,然后输入,也就是会看到输入轨迹。
最后一个清除text,用的是enterTextIntoWebElement方法,就是把value设置为“”,咱们理解成setText(“”)就行了。
public WebElement waitForWebElement(final By by, int minimumNumberOfMatches, int timeout, boolean scroll){
final long endTime = SystemClock.uptimeMillis() + timeout;
while (true) {
final boolean timedOut = SystemClock.uptimeMillis() > endTime;
if (timedOut){
searcher.logMatchesFound(by.getValue());
return null;
}
sleeper.sleep();
WebElement webElementToReturn = searcher.searchForWebElement(by, minimumNumberOfMatches);
if(webElementToReturn != null)
return webElementToReturn;
if(scroll) {
scroller.scrollDown();
}
}
}
这个等待元素出现的比较简单,循环查找并等待超时,自带向下滚动操作,超时就返回null,否则返回找到的元素,当然,这个可以做成断言的,assertthat(xxx,Matcher.isNotNull())用来判断界面元素的展示与否。
好了,分析得差不多了,最后来总结一下。
robotium对webview是支持的,原理是通过获取最近绘制的webview.class作为当前测试的webview,并通过js注入的方式获取webview里面的Element信息,通过prompt回调的方式通讯,获取到元素信息后之后通过onJSPrompt回调取得元素信息,并封装成WebElement对象,最后取得element对象的坐标点调用clickOncreen方法实现点击,当然也可以通过配置config对象让robotium通过dispatchEvent方式点击,不过不是很推荐。其他的类似文本输入可以通过js注入然后domcument的相关api设置value值方式,也可以调用instarumentation的sendKey方式输入文本。