目前比较有名的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. 【转】IOS 学习之 NSPredicate 模糊、精确、查询

    转自:http://blog.csdn.net/lianbaixue/article/details/10579117   简述:Cocoa框架中的NSPredicate用于查询,原理和用法都类似于S ...

  2. Xcode :Missing file warnings

    http://stackoverflow.com/a/5379013

  3. Windows 7运行命令大全

    Windows 7运行命令大全,小编整理了常用的45个Win7运行命令分享给大家:1.cleanmgr:打开磁盘清理工具 2.compmgmt.msc:计算机管理 3.conf:启动系统配置实用程序 ...

  4. hive cst 时间转换

    select from_unixtime(unix_timestamp(r.collecttime,'EEE MMM dd HH:mm:ss zzz yyyy'),'yyyy-MM-dd HH:mm: ...

  5. Java并发包学习一 ThreadFactory介绍

    ThreadFactory翻译过来是线程工厂,顾名思义,就是用来创建线程的,它用到了工厂模式的思想.它通常和线程池一起使用,主要用来控制创建新线程时的一些行为,比如设置线程的优先级,名字等等.它是一个 ...

  6. Redis Crackit漏洞利用和防护

    注意:本文只是阐述该漏洞的利用方式和如何预防.根据职业道德和<中华人民共和国计算机信息系统安全保护条例>,如果发现的别人的漏洞,千万不要轻易入侵,这个是明确的违法的哦!!! 目前Redis ...

  7. 和我一起学《HTTP权威指南》——连接管理

    连接管理 1.TCP连接 几乎所有的HTTP通信都是由TCP/IP承载的. 浏览网页时客户端执行的操作: 如浏览http://www.joes-hardware.com:80/power-tools. ...

  8. mysql查询某个库的表个数

    SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = 'leleli'; --解释:数据库名叫“leleli”

  9. 跨域、跨服务器调用时候session丢失的问题

    最近新进一个公司,做的项目是手机支付系统.由于涉及到金钱相关的,所以安全性要求特别的高.项目分了很多个子系统,在部署(测试)的时候是每个Tomcat上面只放一个子系统.比如现在有5个子系统,那么就会对 ...

  10. [mysql] Incorrect string value: '\xE4\xBC\x9A\xE5\x91\x98' for column 'name' at row 1

    数据库字符集错误, 修改为UTF8/utf8mb4字符集即可.