腾讯Android自动化测试实战
上QQ阅读APP看书,第一时间看更新

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自动化的支持。