目前比较有名的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("\n");
} 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源码的可以多多参考。

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

  1. Appium Android Bootstrap源码分析之控件AndroidElement

    通过上一篇文章<Appium Android Bootstrap源码分析之简介>我们对bootstrap的定义以及其在appium和uiautomator处于一个什么样的位置有了一个初步的 ...

  2. Duilib源码分析(二)控件构造器—CDialogBuilder

    上一节了解了大体流程,但是界面控件元素是如何被加载.解析.构建.管理.控件消息如何处理的呢?接下来我们将结合控件构造器进行分析: CDialogBuilder:控件构造器,主要用以解析xml配置文件并 ...

  3. Robotium源码分析之运行原理

    从上一章<Robotium源码分析之Instrumentation进阶>中我们了解到了Robotium所基于的Instrumentation的一些进阶基础,比如它注入事件的原理等,但Rob ...

  4. Spark jdbc postgresql数据库连接和写入操作源码解读

    概述:Spark postgresql jdbc 数据库连接和写入操作源码解读,详细记录了SparkSQL对数据库的操作,通过java程序,在本地开发和运行.整体为,Spark建立数据库连接,读取数据 ...

  5. AbstractQueuedSynchronizer 源码解读(转载)

    转载文章,拜读了一下原文感觉很不错,转载一下,侵删 链接地址:http://objcoding.com/2019/05/05/aqs-exclusive-lock/ Java并发之AQS源码分析(一) ...

  6. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  7. AFNetworking 3.0 源码解读 总结(干货)(上)

    养成记笔记的习惯,对于一个软件工程师来说,我觉得很重要.记得在知乎上看到过一个问题,说是人类最大的缺点是什么?我个人觉得记忆算是一个缺点.它就像时间一样,会自己消散. 前言 终于写完了 AFNetwo ...

  8. AFNetworking 3.0 源码解读(十一)之 UIButton/UIProgressView/UIWebView + AFNetworking

    AFNetworking的源码解读马上就结束了,这一篇应该算是倒数第二篇,下一篇会是对AFNetworking中的技术点进行总结. 前言 上一篇我们总结了 UIActivityIndicatorVie ...

  9. AFNetworking 3.0 源码解读(十)之 UIActivityIndicatorView/UIRefreshControl/UIImageView + AFNetworking

    我们应该看到过很多类似这样的例子:某个控件拥有加载网络图片的能力.但这究竟是怎么做到的呢?看完这篇文章就明白了. 前言 这篇我们会介绍 AFNetworking 中的3个UIKit中的分类.UIAct ...

随机推荐

  1. Jenkins使用简易教程

    Jenkins是一款能提高效率的软件,它能帮你把软件开发过程形成工作流,典型的工作流包括以下几个步骤 开发 提交 编译 测试 发布 有了Jenkins的帮助,在这5步中,除了第1步,后续的4步都是自动 ...

  2. Lua基础语法讲解

    Lua 是什么? Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能. Lua 是巴西里约热内卢天主教大学( ...

  3. VCard介绍

    91助手和豌豆荚用VCard来存储通讯录,今天调查了一下. 1. 方案 使用VCard存储通讯录,文件扩展名为 vcf,  数据文件可以直接导入IPhone/Windows Phone/android ...

  4. webApi2 结合uploadify 上传报错解决办法

    报错代码: Error reading MIME multipart body part. 处理办法: <httpRuntime targetFramework=" />

  5. 织梦Dedecms系统可疑文件include/filter.inc.php扫描出漏洞,该如何解决?

    今天在做网站监察的时候,发现网站出了一个问题,在对网站做木马监测的时候,扫描出一个可疑文件:/include/filter.inc.php,建议删除,但仔细检查后,发现此文件是织梦(Dedecms)系 ...

  6. 怎样自己定义注解Annotation,并利用反射进行解析

    Java注解可以提供代码的相关信息,同一时候对于所注解的代码结构又没有直接影响.在这篇教程中,我们将学习Java注解,怎样编写自己定义注解.注解的使用,以及怎样使用反射解析注解. 注解是Java 1. ...

  7. [Unity官方文档翻译]Primitive and Placeholder Objects Unity原生3D物体教程

    Primitive and Placeholder Objects 原始的基础物体 Unity can work with 3D models of any shape that can be cre ...

  8. MySQL实现树状所有子节点查询的方法

    本文实例讲述了MySQL实现树状所有子节点查询的方法.分享给大家供大家参考,具体如下: 在Oracle 中我们知道有一个 Hierarchical Queries 通过CONNECT BY 我们可以方 ...

  9. scala中隐式转换之隐式转换调用类中本不存在的方法

    /** * Created by root * Description : 隐式转换调用类中本不存在的方法 */ class Person(name : String){ def getPersonN ...

  10. dedeCMS解码

    var str = 'arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98& ...