zoukankan      html  css  js  c++  java
  • Robotium源码解读-native控件/webview元素的获取和操作

    目前比较有名的Uitest框架有Uiautomator/Robotium/Appium,由于一直对webview元素的获取和操作比较好奇,另外Robotium代码量也不是很大,因此打算学习一下。

    一.环境准备以及初始化

    用来说明的用例采用的是Robotium官网的一个tutorial用例-Notepad

    @RunWith(AndroidJUnit4.class)
    public class NotePadTest {
    
        private static final String NOTE_1 = "Note 1";
        private static final String NOTE_2 = "Note 2";
    
    
        @Rule
        public ActivityTestRule<NotesList> activityTestRule =
                new ActivityTestRule<>(NotesList.class);
    
        private Solo solo;
    
    
        @Before
        public void setUp() throws Exception {
            //setUp() is run before a test case is started.
            //This is where the solo object is created.
            solo = new Solo(InstrumentationRegistry.getInstrumentation(),
                    activityTestRule.getActivity());
        }
    
        @After
        public void tearDown() throws Exception {
            //tearDown() is run after a test case has finished.
            //finishOpenedActivities() will finish all the activities that have been opened during the test execution.
            solo.finishOpenedActivities();
        }
    
        @Test
        public void testAddNote() throws Exception {
            //Unlock the lock screen
            solo.unlockScreen();
            //Click on action menu item add
            solo.clickOnView(solo.getView(R.id.menu_add));
            //Assert that NoteEditor activity is opened
            solo.assertCurrentActivity("Expected NoteEditor Activity", NoteEditor.class);
            //In text field 0, enter Note 1
            solo.enterText(0, NOTE_1);
            //Click on action menu item Save
            solo.clickOnView(solo.getView(R.id.menu_save));
            //Click on action menu item Add
            solo.clickOnView(solo.getView(R.id.menu_add));
            //In text field 0, type Note 2
            solo.typeText(0, NOTE_2);
            //Click on action menu item Save
            solo.clickOnView(solo.getView(R.id.menu_save));
            //Takes a screenshot and saves it in "/sdcard/Robotium-Screenshots/".
            solo.takeScreenshot();
            //Search for Note 1 and Note 2
            boolean notesFound = solo.searchText(NOTE_1) && solo.searchText(NOTE_2);
            //To clean up after the test case
            deleteNotes();
            //Assert that Note 1 & Note 2 are found
            assertTrue("Note 1 and/or Note 2 are not found", notesFound);
        }
    
    }

    在进行初始化时,Solo对象依赖Instrumentation对象以及被测应用的Activity对象,在这里是NotesList,然后所有的UI操作都依赖这个Solo对象。

    二.Native控件解析与操作

    1.Native控件解析

    看一个标准的操作:solo.clickOnView(solo.getView(R.id.menu_save));

    solo点击id为menu_save的控件,其中clickOnView传入参数肯定为menu_save的view对象,那这个是怎么获取的呢?

    由于调用比较深,因此直接展示关键方法

        public View waitForView(int id, int index, int timeout, boolean scroll) {
            HashSet uniqueViewsMatchingId = new HashSet();
            long endTime = SystemClock.uptimeMillis() + (long)timeout;
    
            while(SystemClock.uptimeMillis() <= endTime) {
                this.sleeper.sleep();
                Iterator i$ = this.viewFetcher.getAllViews(false).iterator();
    
                while(i$.hasNext()) {
                    View view = (View)i$.next();
                    Integer idOfView = Integer.valueOf(view.getId());
                    if(idOfView.equals(Integer.valueOf(id))) {
                        uniqueViewsMatchingId.add(view);
                        if(uniqueViewsMatchingId.size() > index) {
                            return view;
                        }
                    }
                }
    
                if(scroll) {
                    this.scroller.scrollDown();
                }
            }
    
            return null;
        }

    这个方法是先去获取所有的View: this.viewFetcher.getAllViews(false),然后通过匹配id来获取正确的View。

    那Robotium是怎么获取到所有的View呢?这就要看看viewFetcher里的实现了。

        public ArrayList<View> getAllViews(boolean onlySufficientlyVisible) {
            View[] views = this.getWindowDecorViews();
            ArrayList allViews = new ArrayList();
            View[] nonDecorViews = this.getNonDecorViews(views);
            View view = null;
            if(nonDecorViews != null) {
                for(int ignored = 0; ignored < nonDecorViews.length; ++ignored) {
                    view = nonDecorViews[ignored];
    
                    try {
                        this.addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);
                    } catch (Exception var9) {
                        ;
                    }
    
                    if(view != null) {
                        allViews.add(view);
                    }
                }
            }
    
            if(views != null && views.length > 0) {
                view = this.getRecentDecorView(views);
    
                try {
                    this.addChildren(allViews, (ViewGroup)view, onlySufficientlyVisible);
                } catch (Exception var8) {
                    ;
                }
    
                if(view != null) {
                    allViews.add(view);
                }
            }
    
            return allViews;
        }

    需要说明的是,DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面。

    从上面的代码可以看到,allViews包括WindowDecorViews,nonDecorViews,RecentDecorView。所以,我对这三个封装比较感兴趣,他们是怎么拿到WindowDecorViews,nonDecorViews,RecentDecorView的呢?

    继续看代码,可以看到以下方法(看注释)

       // 获取 DecorViews
       public View[] getWindowDecorViews() {
            try {
                Field viewsField = windowManager.getDeclaredField("mViews");
                Field instanceField = windowManager.getDeclaredField(this.windowManagerString);
                viewsField.setAccessible(true);
                instanceField.setAccessible(true);
                Object e = instanceField.get((Object)null);
                View[] result;
                if(VERSION.SDK_INT >= 19) {
                    result = (View[])((ArrayList)viewsField.get(e)).toArray(new View[0]);
                } else {
                    result = (View[])((View[])viewsField.get(e));
                }
    
                return result;
            } catch (Exception var5) {
                var5.printStackTrace();
                return null;
            }
        }
    
        // 获取NonDecorViews
        private final View[] getNonDecorViews(View[] views) {
            View[] decorViews = null;
            if(views != null) {
                decorViews = new View[views.length];
                int i = 0;
    
                for(int j = 0; j < views.length; ++j) {
                    View view = views[j];
                    if(!this.isDecorView(view)) {
                        decorViews[i] = view;
                        ++i;
                    }
                }
            }
    
            return decorViews;
        }
    
    
        // 获取RecentDecorView
        public final View getRecentDecorView(View[] views) {
            if(views == null) {
                return null;
            } else {
                View[] decorViews = new View[views.length];
                int i = 0;
    
                for(int j = 0; j < views.length; ++j) {
                    View view = views[j];
                    if(this.isDecorView(view)) {
                        decorViews[i] = view;
                        ++i;
                    }
                }
    
                return this.getRecentContainer(decorViews);
            }
        }

    其中DecorViews就不用多说了,通过反射拿到一个里面的元素都是View的List,而NonDecorViews则是通过便利DectorViews进行过滤,nameOfClass 不满足要求的,则为NonDecorViews

    String nameOfClass = view.getClass().getName();
    return nameOfClass.equals("com.android.internal.policy.impl.PhoneWindow$DecorView") || nameOfClass.equals("com.android.internal.policy.impl.MultiPhoneWindow$MultiPhoneDecorView") || nameOfClass.equals("com.android.internal.policy.PhoneWindow$DecorView");

    而recentlyView则通过以下条件进行判断,满足则为recentlyView

    view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime

    2.Native控件解析

    依旧说的是这个操作:solo.clickOnView(solo.getView(R.id.menu_save));接下来要看的是clickOnView的封装了。

    这部分实现相对简单很多了,获取控件坐标的中央X,Y值后,利用instrumentation的sendPointerSync来完成点击/长按操作

        public void clickOnScreen(float x, float y, View view) {
            boolean successfull = false;
            int retry = 0;
            SecurityException ex = null;
    
            while(!successfull && retry < 20) {
                long downTime = SystemClock.uptimeMillis();
                long eventTime = SystemClock.uptimeMillis();
                MotionEvent event = MotionEvent.obtain(downTime, eventTime, 0, x, y, 0);
                MotionEvent event2 = MotionEvent.obtain(downTime, eventTime, 1, x, y, 0);
    
                try {
                    this.inst.sendPointerSync(event);
                    this.inst.sendPointerSync(event2);
                    successfull = true;
                } catch (SecurityException var16) {
                    ex = var16;
                    this.dialogUtils.hideSoftKeyboard((EditText)null, false, true);
                    this.sleeper.sleep(300);
                    ++retry;
                    View identicalView = this.viewFetcher.getIdenticalView(view);
                    if(identicalView != null) {
                        float[] xyToClick = this.getClickCoordinates(identicalView);
                        x = xyToClick[0];
                        y = xyToClick[1];
                    }
                }
            }
    
            if(!successfull) {
                Assert.fail("Click at (" + x + ", " + y + ") can not be completed! (" + (ex != null?ex.getClass().getName() + ": " + ex.getMessage():"null") + ")");
            }
    
        }

    3.总结:

    从源码中可以看出,其实native控件操作的思想是这样的。

    通过android.view.windowManager获取到所有的view,然后经过过滤得到自己需要的view,最后通过计算view的 Coordinates得到中央坐标,最后依赖instrument来完成操作。

    三.Webview的解析与操作

    webview的解析需要利用JS注入获取到Web页面的元素,进行过滤后再进行操作。

    webview的调试环境可以利用inspect来进行,具体参考此文章:http://www.cnblogs.com/sunshq/p/4111610.html

    在这里进行说明的解析操作代码为:

    ArrayList<WebElement> webElements = solo.getCurrentWebElements(By.className("ns-video ns-icon")); 
    solo.clickOnWebElement(webElements.get(
    0));

    这段代码很好理解,取出className为"btn btn-block primary"的元素,并点击第一个。在这里,元素的可操作对象为WebElement.

    debug界面为:

    在具体debug代码前,我们有必要先了解一下解析Webview的流程应该是怎样的(尽管我是看代码了解的,但先把流程说一下方便解说),不然很可能会云里雾里。流程如下:

    1. 获取所有的view,过滤出webview

    2.初始化javascript环境

    3.加载本地js并注入

    4.WebElment操作

    接下来,自然而然,带着目的去看代码,就可以很清楚每一步在做什么了。

    1. 获取所有的view,过滤出webview

    (1)直接跳到关键代码,首先要看的是By是用来干嘛的。通过查看源码,可以发现,其实By是一个Java bean,里面封装了Id/CssSelector/ClassName/Text等等属性,可以理解为WebView中的目标对象封装。

        public boolean executeJavaScript(By by, boolean shouldClick) {
            return by instanceof Id?this.executeJavaScriptFunction("id("" + by.getValue() + "", "" + shouldClick + "");"):(by instanceof Xpath?this.executeJavaScriptFunction("xpath("" + by.getValue() + "", "" + shouldClick + "");"):(by instanceof CssSelector?this.executeJavaScriptFunction("cssSelector("" + by.getValue() + "", "" + shouldClick + "");"):(by instanceof Name?this.executeJavaScriptFunction("name("" + by.getValue() + "", "" + shouldClick + "");"):(by instanceof ClassName?this.executeJavaScriptFunction("className("" + by.getValue() + "", "" + shouldClick + "");"):(by instanceof Text?this.executeJavaScriptFunction("textContent("" + by.getValue() + "", "" + shouldClick + "");"):(by instanceof TagName?this.executeJavaScriptFunction("tagName("" + by.getValue() + "", "" + shouldClick + "");"):false))))));
        }
    
        private boolean executeJavaScriptFunction(final String function) {
            ArrayList webViews = this.viewFetcher.getCurrentViews(WebView.class, true);
            final WebView webView = (WebView)this.viewFetcher.getFreshestView((ArrayList)webViews);
            if(webView == null) {
                return false;
            } else {
                final String javaScript = this.setWebFrame(this.prepareForStartOfJavascriptExecution(webViews));
                this.inst.runOnMainSync(new Runnable() {
                    public void run() {
                        if(webView != null) {
                            webView.loadUrl("javascript:" + javaScript + function);
                        }
    
                    }
                });
                return true;
            }
        }

    executeJavaScript获取到的是对应的执行方法调用语句,这个根据自己定位的元素决定,在这,我的为:"className("ns-video ns-icon", "false");"

    (2)getCurrentViews,获取所有当前View

        public <T extends View> ArrayList<T> getCurrentViews(Class<T> classToFilterBy, boolean includeSubclasses, View parent) {
            ArrayList filteredViews = new ArrayList();
            ArrayList allViews = this.getViews(parent, true);
            Iterator i$ = allViews.iterator();
    
            while(true) {
                View view;
                Class classOfView;
                do {
                    do {
                        if(!i$.hasNext()) {
                            allViews = null;
                            return filteredViews;
                        }
    
                        view = (View)i$.next();
                    } while(view == null);
    
                    classOfView = view.getClass();
                } while((!includeSubclasses || !classToFilterBy.isAssignableFrom(classOfView)) && (includeSubclasses || classToFilterBy != classOfView));
    
                filteredViews.add(classToFilterBy.cast(view));
            }
        }

    其中classToFilterBy为android.webkit.Webview这个类,所作的操作为调用获取所有View(跟navitive调用的方法一致),包括控件view跟webView,如图所示

     

    然后逐一过滤出,条件为(!includeSubclasses || !classToFilterBy.isAssignableFrom(classOfView)) && (includeSubclasses || classToFilterBy != classOfView))。在这里加个题外话,因为robotium默认的是android.webkit.Webview,因此如果你用robotium去解析操作第三方内核的Webview,是会失败的,解决办法就是改源码?

    2.初始化javascript环境

    看(1)的代码:this.setWebFrame(this.prepareForStartOfJavascriptExecution(webViews));

    在这里会初始化一个robotiumWebCLient, 并将webChromeClient设置成RobotiumWebClient.this.robotiumWebClient,由于我对这一块也不熟悉,不太清楚这一块的目的,因此不赘述,姑且认为是执行js注入的环境初始化。 

    3.加载js脚本并注入

        private String getJavaScriptAsString() {
            InputStream fis = this.getClass().getResourceAsStream("RobotiumWeb.js");
            StringBuffer javaScript = new StringBuffer();
    
            try {
                BufferedReader e = new BufferedReader(new InputStreamReader(fis));
                String line = null;
    
                while((line = e.readLine()) != null) {
                    javaScript.append(line);
                    javaScript.append("
    ");
                }
    
                e.close();
                return javaScript.toString();
            } catch (IOException var5) {
                throw new RuntimeException(var5);
            }
        }

     这一块就没多少要说的了,就是把本地的js脚本加载进来,方便执行,最后在异步线程中将js注入,参考(1)中的webView.loadUrl("javascript:" + javaScript + function);

    在这里可以展示一下我这边注入的js是怎样的(只展示结构,具体方法内容略掉):

    javascript:/**
     * Used by the web methods.
     * 
     * @author Renas Reda, renas.reda@robotium.com
     * 
     */
    
    function allWebElements() {
        ...
    }
    
    function allTexts() {
        ...
    }
    
    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);
    }
    
    function id(id, click) {
        ...
    }
    
    function xpath(xpath, click) {
        ...
    }
    
    function cssSelector(cssSelector, click) {
        ...
    }
    
    function name(name, click) {
        ...
    }
    
    function className(nameOfClass, click) {
        ... 
    }
    
    function textContent(text, click) {
        ...
    }
    
    function tagName(tagName, click) {
        ...
    }
    
    function enterTextById(id, text) {
        ...
    }
    
    function enterTextByXpath(xpath, text) {
        ...
    }
    
    function enterTextByCssSelector(cssSelector, text) {
        ... 
    }
    
    function enterTextByName(name, text) {
        ...
    }
    
    function enterTextByClassName(name, text) {
        ...
    }
    
    function enterTextByTextContent(textContent, text) {
        ...
    }
    
    function enterTextByTagName(tagName, text) {
        ...
    }
    
    function promptElement(element) {
        ...
    }
    
    function promptText(element, range) {    
        ...
    }
    
    function finished(){
        prompt('robotium-finished');
    }
    className("ns-video ns-icon", "false");

    4.WebElment操作

    接下来便是元素操作了,在这里的操作对象是WebElment,获取到下X,Y坐标进行对应操作就可以了。

    总结:

    这篇文章展示了robotium是如何去识别控件跟webview元素的,这个基本上是一个框架能用与否的关键,有兴趣造轮子或者想学习robotium源码的可以多多参考。

     

  • 相关阅读:
    关于EF中实体和数据表以及查询语句映射的问题
    流程设计(流程上下文法)
    流程设计(抽象节点法)
    第六章 跑马灯实验
    如何批量导入excel数据至数据库(MySql)--工具phpMyAdmin
    win10卸载瑞星
    如何在sublime上运行php
    Pycharm+django新建Python Web项目
    部署Asp.net Core 项目发生502.5 或者500 没有其他提示信息
    常用Windows DOS命令项目部署经常用到
  • 原文地址:https://www.cnblogs.com/alexkn/p/6505785.html
Copyright © 2011-2022 走看看