Robotium源码解读-native/webview控件的获取和操作

之前基本上没接触过移动端的UITest测试,之前因为一些需求临时赶鸭子上架采用了UIAutomator,但是后来发现webview没办法识别,在预研过程中,发现Robotium跟Appium这两个神器。由于Robotium提供了webview的解析方式,遂决定研究一下。

一.环境准备以及初始化

用来说明的用例采用的是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);
    }

    @Test
    public void testEditNoteTitle() throws Exception {
        //Click on add action menu item
        solo.clickOnView(solo.getView(com.example.android.notepad.R.id.menu_add));
        //In text field 0, enter Note 1
        solo.enterText(0, NOTE_1);
        //Press hard key back button
        solo.goBack();
        solo.clickOnText(NOTE_1);
        //Click on menu item "Edit title"
        solo.clickOnMenuItem("Edit title");
        //Clear the edit text field
        solo.clearEditText(0);
        //In the text field enter Note 2
        solo.enterText(0, NOTE_2);
        //Click on button "OK"
        solo.clickOnButton("OK");
        //Click on action menu item Save
        solo.clickOnView(solo.getView(com.example.android.notepad.R.id.menu_save));
        //Long click Note 2
        solo.clickLongOnText(NOTE_2);
        //Click on Delete
        solo.clickOnText("Delete");
        //Assert that Note 2 is deleted
        assertFalse("Note 2 is found", solo.searchText(NOTE_2));
    }

    private void deleteNotes() {
        //Click on first item in List
        solo.clickInList(1);
        //Click on delete action menu item
        solo.clickOnView(solo.getView(com.example.android.notepad.R.id.menu_delete));
        //Long click first item in List
        solo.clickLongInList(1);
        //Click delete
        solo.clickOnText(solo.getString(R.string.menu_delete));
    }
}

在进行初始化时,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的解析与操作

时间: 2024-08-06 20:08:56

Robotium源码解读-native/webview控件的获取和操作的相关文章

Duilib 学习源码系列1-创建控件

好了,昨天研究出了为什么加载xml结束以后我在自己新建一个控件位置不能调整,原来要先add才能调属性. 本来这个是昨天的任务,虽然这块内容是前天就看完的,权当边写边复习吧. 上一篇提到 <VerticalLayout name="window" bkcolor="#FFFFFFFF" bkcolor2="#FFAAAAA0" bkcolor3="#00000000"> 代表了一个控件字符串; 上次忘记说了 及时经过

JGUI源码:实现日期控件显示(17)

本文实现一个日期控件显示,日期控件看起来很复杂,其实原理很简单,为了使程序逻辑看起来简单,切换日期,选择日期等事件处理部分未实现,读者可以自己尝试实现. 1.日期控件分为三个区域:顶部的显示当前日期和选择按钮区域:中间的本月日期显示列表,固定7*6=42个单元格: 底部确定.取消.当前日期选择功能. 2.思路主要是:计算出应该显示的单元格内容,然后替换body区域即可. 代码如下 <style> .jgui-datetimepicker { padding: 10px; } .jgui-dat

Robotium源码分析之运行原理

从上一章<Robotium源码分析之Instrumentation进阶>中我们了解到了Robotium所基于的Instrumentation的一些进阶基础,比如它注入事件的原理等,但Robotium作为一个测试框架,其功能远不止于只是方便我们注入事件,其应该还包含其他高级的功能,参照我们前面其他框架如MonkeyRunner,UiAutomator和Appium的源码分析,我们知道一个移动平台自动化测试框架的基本功能除了事件注入外起码还应该有控件获取的功能.所以,这篇文章我们主要是围绕Robo

PhotoView 源码解读

开源库地址:https://github.com/chrisbanes/PhotoView PhotoView是一个用来帮助开发者轻松实现ImageView缩放的库.开发者可以轻易控制对图片的缩放旋等等操作. PhotoView的使用极其简单,而且提供了两种方案.可以使用普通的ImageView,也可以使用该库中提供的ImageView(PhotoView). 使用PhotoView 只需如下引用该库中的ImageView,无需关心其它实现细节,你的ImageView便可拥有缩放效果. <uk.

RequireJs 源码解读及思考

写在前面: 最近做的一个项目,用的require和backbone,对两者的使用已经很熟悉了,但是一直都有好奇他们怎么实现的,一直寻思着读读源码.现在项目结束,终于有机会好好研究一下. 本文重要解读requirejs的源码,backbone源码分析将会随后给出. 行文思路: requirejs 基本介绍 requirejs使用后的几个好奇 requirejs源码解读 requirejs基本介绍 由于篇幅有限,这里不会详解requirejs的使用和api,建议读者朋友自己去用几次,再详读api.

Android-Universal-Image-Loader 源码解读

Universal-Image-Loader是一个强大而又灵活的用于加载.缓存.显示图片的Android库.它提供了大量的配置选项,使用起来非常方便. 基本概念 基本使用 首次配置 在第一次使用ImageLoader时,必须初始化一个全局配置,一般会选择在Application中配置. public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); //为I

AFNetworking 3.0 源码解读 总结

终于写完了 AFNetworking 的源码解读.这一过程耗时数天.当我回过头又重头到尾的读了一篇,又有所收获.不禁让我想起了当初上学时的种种情景.我们应该对知识进行反复的记忆和理解.下边是我总结的 AFNetworking 中能够学到的知识点. 1.枚举(enum) 使用原则:当满足一个有限的并具有统一主题的集合的时候,我们就考虑使用枚举.这在很多框架中都验证了这个原则.最重要的是能够增加程序的可读性. 示例代码: /** * 网络类型 (需要封装为一个自己的枚举) */ typedef NS

fastclick.js源码解读分析

阅读优秀的js插件和库源码,可以加深我们对web开发的理解和提高js能力,本人能力有限,只能粗略读懂一些小型插件,这里带来对fastclick源码的解读,望各位大神不吝指教~! fastclick诞生背景与使用 在解读源码前,还是简单介绍下fastclick: 诞生背景 我们都知道,在移动端页面开发上,会出现一个问题,click事件会有300ms的延迟,这让用户感觉很不爽,感觉像是网页卡顿了一样,实际上,这是浏览器为了更好的判断用户的双击行为,移动浏览器都支持双击缩放或双击滚动的操作,比如一个链

Java之ArrayList源码解读(JDK 1.8)

java.util.ArrayList 详细注释了ArrayList的实现,基于JDK 1.8 . 迭代器SubList部分未详细解释,会放到其他源码解读里面.此处重点关注ArrayList本身实现. 没有采用标准的注释,并适当调整了代码的缩进以方便介绍 import java.util.AbstractList; import java.util.Arrays; import java.util.BitSet; import java.util.Collection; import java.