3.2.2 Robotium支持WebView原理
在上一节中我们介绍了在Robotium中如何通过By.id或By.className方式获取Web-Element,那么Robotium中是如何获取到相应的HTML元素,并能知道元素坐标,从而发送点击事件的呢?
1.WebElement对象
Robotium中以WebElement对象对HTML元素进行了封装,在这个WebElement对象中包含locationX、locationY、ID、text、name、className、tagName等信息。
❑ locationX、locationY:标识该HTML元素在屏幕中所在的X坐标和Y坐标。
❑ ID、className:该HTML元素的属性。
❑ tagName:该HTML元素的标签。
Robotium中封装了WebElement,提供了clickOnWebElement(WebElement webElement), ArrayList<WebElement> getCurrentWebElements()等操作Web元素的API,对于在Android客户端中展示的Web页面,Robotium是如何把里面的元素都提取出来,并封装进WebElement对象中的呢?
如图3-13所示,通过getWebElements方法的调用关系图可以看出,Robotium主要通过JS注入的方式获取Web页面所有的元素,再对这些元素进行提取并封装成WebElement对象。在Android端与JS交互则离不开WebView和WebCromeClient。
图3-13 getWebElements方法的调用关系图
2.WebElement元素获取
1)利用JS获取页面中的所有元素
在PC上,获取网页的元素可以通过注入javascript元素来完成,以Chrome浏览器为例,打开工具—JavaScript控制台(快捷方式:Ctrl+Shift+J键),输入javascript:prompt (document.URL)即会弹出含当前页面的URL的提示框,因此通过编写适当的JS脚本就可以在这个弹出框中显示所有的页面元素。RobotiumWeb.js就提供了获取所有HTML元素的JS脚本。以Solo中getWebElements()为例,如代码清单3-12所示,可分为两步,先通过executeJavaScriptFunction()方法执行JS脚本,然后根据执行结果通过getWebElements返回。
代码清单3-12 WebUtils.getWebElements
public ArrayList<WebElement> getWebElements(boolean onlySufficientlyVisible){ boolean javaScriptWasExecuted = executeJavaScriptFunction("allWebElements(); "); return getWebElements(javaScriptWasExecuted, onlySufficientlyVisible); }
如代码清单3-13所示,在executeJavaScriptFunction(final String function)方法中通过webView.loadUrl(String url)方法执行JS,而这里的WebView是通过getCurrentViews (Class<T> classToFilterBy, boolean includeSubclasses)过滤出来的,且是过滤的android. webkit.WebView,这也是Robotium只支持系统WebView而不支持第三方浏览内核中的WebView的原因:
代码清单3-13 WebUtils.executeJavaScriptFunction
private boolean executeJavaScriptFunction(final String function) { List<WebView> webViews = viewFetcher.getCurrentViews(WebView.class, true); //获取当前屏幕中最新的WebView,即目标要执行JS的WebView //注:这里获取的WebView可能不是目标WebView,那么将导致获取WebElement失败 final WebView webView = viewFetcher.getFreshestView((ArrayList<WebView>) webViews); if(webView == null) { return false; } //执行JS前的准备工作,如设置WebSettings、获取JS方法等 final String javaScript = setWebFrame(prepareForStartOfJavascriptExecution(w ebViews)); inst.runOnMainSync(new Runnable() { public void run() { if(webView ! = null){ //调用loadUrl执行JS webView.loadUrl("javascript:" + javaScript + function); } } }); return true; }
想返回什么样的结果,关键在于执行了什么样的JS方法,Robotium中的getWeb-Elements()执行的JS方法是allWebElements(),代码片段可以通过RobotiumWeb.js找到,如代码清单3-14所示,采用遍历DOM的形式获取所有的元素信息。
代码清单3-14 RobotiumWeb.js中的allWebElements()
function allWebElements() { for (var key in document.all){ try{ promptElement(document.all[key]); }catch(ignored){} } finished(); }
如代码清单3-15所示,将代码清单3-15中遍历获取到的每一个元素分别获取ID、text、className等,然后将元素通过prompt方法以提示框形式显示。在prompt时,会在ID、text、className等字段之间加上’; , ’特殊字符,以便解析时区分这几个字段。
代码清单3-15 RobotiumWeb.js中的promptElement(element)
function promptElement(element) { var id = element.id; var text = element.innerText; if(text.trim().length == 0){ text = element.value; } var name = element.getAttribute('name'); var className = element.className; var tagName = element.tagName; var attributes = ""; var htmlAttributes = element.attributes; for (var i = 0, htmlAttribute; htmlAttribute = htmlAttributes[i]; i++){ attributes += htmlAttribute.name + "::" + htmlAttribute.value; if (i + 1 < htmlAttributes.length) { attributes += "#$"; } } var rect = element.getBoundingClientRect(); if(rect.width > 0 && rect.height > 0 && rect.left >= 0 && rect.top >= 0){ prompt(id + '; , ' + text + '; , ' + name + "; , " + className + "; , " + tagName + "; , " + rect.left + '; , ' + rect.top + '; , ' + rect.width + '; , ' + rect. height + '; , ' + attributes); } }
最后,执行finished()方法,调用prompt提示框,提示语为特定的’robotium-finished',用于在Robotium执行JS时,判断是否执行完毕,如代码清单3-16所示。
代码清单3-16 RobotiumWeb.js中的finished()
function finished(){ //robotium-finished用来标识Web元素遍历结束 prompt('robotium-finished'); }
通过JS完成了Web页面所有元素的提取,提取的所有元素是以prompt方式显示在提示框中的,那么提示框中包含的内容在Android中怎么获取呢?
2)通过onJsPrompt回调获取prompt提示框中的信息
如代码清单3-17所示,通过JS注入获取到Web页面所有的元素后,可以通过onJsPrompt回调来对这些元素进行提取。Robotium写了个继承自WebChromeClient类的RobotiumWebClient类,覆写了onJsPrompt用于回调提取元素信息,如果提示框中包含“robotium-finished”字符串,即表示这段JS脚本执行完毕了,此时通知webElementCreator可以停止等待,否则,将不断将prompt框中的信息交由webElementCreator.createWeb-ElementAndAddInList解析处理。
代码清单3-17 RobotiumWebClient中的onJsPrompt
@Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult r) { //当message包含robotium-finished时,表示JS执行结束 if(message ! = null && (message.contains("; , ") || message.contains("robotium- finished"))){ if(message.equals("robotium-finished")){ //setFinished为true后,WebElementCreator将停止等待 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; } }
3)将回调中获取的元素信息封装进WebElement对象中
获取到onJsPrompt回调中的元素信息后,接下来就可以对这些已经过处理、含特殊格式的消息进行解析了,依次得到WebElement的ID、text、name等字段。如代码清单3-18所示,将information通过特殊字符串“; , ”分隔成数组对该字符串进行分段解析,将解析而得的ID、text、name及x, y坐标存储至WebElement对象中。
代码清单3-18 WebElementCreator中的createWebElementAndSetLocation
private WebElement createWebElementAndSetLocation(String information, WebView webView){ //将information通过特殊字符串“; , ”分隔成数组 String[] data = information.split("; , "); String[] elements = null; int x = 0; int y = 0; int width = 0; int height = 0; Hashtable<String, String> attributes = new Hashtable<String, String>(); try{ x = Math.round(Float.valueOf(data[5])); y = Math.round(Float.valueOf(data[6])); width = Math.round(Float.valueOf(data[7])); height = Math.round(Float.valueOf(data[8])); elements = data[9].split("\\#\\$"); }catch(Exception ignored){} if(elements ! = null) { for (int index = 0; index < elements.length; index++){ String[] element = elements[index].split("::"); if (element.length > 1) { attributes.put(element[0], element[1]); } else { attributes.put(element[0], element[0]); } } } WebElement webElement = null; try{ //设置WebElement中的各个字段 webElement = new WebElement(data[0], data[1], data[2], data[3], data[4], attributes); setLocation(webElement, webView, x, y, width, height); }catch(Exception ignored) {} return webElement; }
这样,把JS执行时提取到的所有元素信息解析出来,并储存至WebElement对象中,在获取到相应的WebElement对象后,就包括了元素的ID、text、className等属性及其在屏幕中的坐标,完成了对Web自动化的支持。