Espresso编写自动化就做三件事情:找某些东西、做某些事情、检查某些东西
- 找到 并返回 XXInteraction交互类
- public static ViewInteraction onView(final Matcher< View > viewMatcher)
- public static DataInteraction onData(Matcher< ? extends Object> dataMatcher)
让我们先来看看Matchers 都有哪些API可供我们使用
- Classes:BoundedMatcher< T, S extends T >
Some matcher sugar that lets you create a matcher for a given type but only process items of a specific subtype of that matcher.
- Classes:CursorMatchers
A collection of Hamcrest matchers that matches a data row in a Cursor.
- Classes:CursorMatchers.CursorMatcher
A Matcher that matches Cursors based on values in their columns.
- Classes:LayoutMatchers
A collection of hamcrest matches to detect typical layout issues.
- Classes:PreferenceMatchers
A collection of hamcrest matchers that match Preferences.
- Classes:RootMatchers
A collection of matchers for Root objects.
- Classes:ViewMatchers
A collection of hamcrest matchers that match Views.
- Enums:ViewMatchers.Visibility
ViewMatchers.Visibility Enumerates the possible list of values for View.getVisibility()
一、源码分析
- 所有的校验都 实现了 interface Matcher
public interface Matcher extends SelfDescribing {
/**
* 遍历当前视图中的Views,匹配到则返回true
*/
boolean matches(Object item);
/**
* 描述信息
*/
void describeMismatch(Object item, Description mismatchDescription);
}
当然在实际过程中,我们没有直接去实现Matcher,而是实现其子类BaseMatcher所派生的封装类。
public abstract class BaseMatcher<T> implements Matcher<T>
譬如:
//进行类型校验
abstract class TypeSafeMatcher<T> extends BaseMatcher<T>
1.1 TypeSafeMatcher
直接上两个例子吧:
/**
* 返回具有指定 Tag 的View
*/
public static Matcher<View> withTag(final Object tag) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("with key: " + tag);
}
@Override
public boolean matchesSafely(View view) {
return tag.equals(view.getTag());
}
};
}
/**
* 在X中的 满足Y 的 X的直系子节点
* @param parentMatcher 目标view的直接父节点
* @param childtMatcher 目标view满足
* @return
*/
public static Matcher<View> childWithMatcher(
final Matcher<View> parentMatcher, final Matcher<View> childtMatcher) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("Child with childtMatcher in parent ");
parentMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(View view) {
ViewParent parent = view.getParent();
return parent instanceof ViewGroup && parentMatcher.matches(parent)
&& childtMatcher.matches(view);
}
};
}
1.2 BoundedMatcher
相对于 TypeSafeMatcher,BoundedMatcher 允许您为给定类型创建匹配器,但只能处理该匹配器的特定子类型项;
从而不需要关注类型项,而只关注 对象的具体业务
public static Matcher<View> withText(final Matcher<String> stringMatcher) {
checkNotNull(stringMatcher);
return new BoundedMatcher<View, TextView>(TextView.class) {
@Override
public void describeTo(Description description) {
description.appendText("with text: ");
stringMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(TextView textView) {
return stringMatcher.matches(textView.getText().toString());
}
};
}
- BoundedMatcher来确保匹配器只匹配TextView类及其子类
- 这使得很容易在BoundedMatcher.matchesSafely()中实现匹配逻辑本身:只需从TextView中获取getText()方法并将其送入下一个匹配器
- 有一个简单的describeTo()方法的实现,它只用于生成调试输出到控制台。
二、工具集
2.1 ViewMatchers View匹配器适配类
最重要也是应用最广的匹配器,通过一个或者多个来定位层级里面的控件。
包含了大量的常见的匹配操作,大部分都是通过TypeSafeMatcher 和 BoundedMatcher实现的..
返回值 | 函数 | 示意 | matchesSafely | 原文 |
---|---|---|---|---|
static void | assertThat(String message, T actual, Matcher matcher) | A replacement for MatcherAssert.assertThat that renders View objects nicely. | ||
static void | assertThat(T actual, Matcher matcher) | A replacement for MatcherAssert.assertThat that renders View objects nicely. | ||
static Matcher | hasContentDescription() | 获取具有ContentDescription的view | view.getContentDescription() != null | Returns an Matcher that matches Views with any content description. |
static Matcher | hasDescendant(Matcher descendantMatcher) | 获取具有“descendantMatcher满足的View”作为直接或间接子节点的VIew | … | Returns a matcher that matches Views based on the presence of a descendant in its view hierarchy. |
static Matcher | hasErrorText(String expectedError) | 获取是 EditText 及其子类类型的,并且getError()== expectedError 的View | hasErrorText(is(expectedError)) | Returns a matcher that matches EditText based on edit text error string value. |
static Matcher | hasErrorText(Matcher stringMatcher) | 获取是 EditText 及其子类类型的,并且getError()满足stringMatcher的View | stringMatcher.matches(view.getError().toString()) | Returns a matcher that matches EditText based on edit text error string value. |
static Matcher | hasFocus() | 获取持有焦点的View | view.hasFocus() | Returns a matcher that matches Views currently have focus. |
static Matcher | hasImeAction(int imeAction) | 支持输入的,并且具有指定imeAction的View | … | Returns a matcher that matches views that support input methods (e.g. EditText) and have the* specified IME action set in its {@link EditorInfo} |
static Matcher | hasImeAction(Matcher imeActionMatcher) | 支持输入的,并且满足imeActionMatcher的View | … | Returns a matcher that matches views that support input methods (e.g… |
static Matcher | hasLinks() | 获取是TextView及其子类类型的,并且具有出超链接的View | textView.getUrls().length > 0 | Returns a matcher that matches TextViews that have links. |
static Matcher | hasSibling(Matcher siblingMatcher) | 获取 在View层级上具有“满足siblingMatcher”作为直接或间接兄弟的View | siblingMatcher.matches(parentGroup.getChildAt(i)) | Returns an Matcher that matches Views based on their siblings. |
static Matcher | isAssignableFrom(Class clazz) | 获取 是clazz子类的View | clazz.isAssignableFrom(view.getClass()) | Returns a matcher that matches Views which are an instance of or subclass of the provided class. |
static Matcher | isChecked() | 获取实现了Checkable接口的,并且被选中的View | withCheckBoxState(is(true)) | Returns a matcher that accepts if and only if the view is a CompoundButton (or subtype of) and is in checked state. |
static Matcher | isClickable() | 获取可以点击的View | view.isClickable() | Returns a matcher that matches Views that are clickable. |
static Matcher | isCompletelyDisplayed() | 获取正在完全显示的VIew,不包括部分显示 | isDisplayingAtLeast(100) | Returns a matcher which only accepts a view whose height and width fit perfectly within the currently displayed region of this view. |
static Matcher | isDescendantOfA(Matcher ancestorMatcher) | 获取 拥有”满足ancestorMatcher的view”的作为父辈节点的View | checkAncestors(ViewParent viewParent, Matcher ancestorMatcher) | Returns a matcher that matches Views based on the given ancestor type. |
static Matcher | isDisplayed() | 获取正在显示的VIew,包括部分显示 | view.getGlobalVisibleRect(new Rect())&& withEffectiveVisibility(Visibility.VISIBLE).matches(view) | Returns a matcher that matches Views that are currently displayed on the screen to the user. |
static Matcher | isDisplayingAtLeast(int areaPercentage) | 获取至少有areaPercentage百分比在显示的View | … | Returns a matcher which accepts a view so long as a given percentage of that view’s area is not obscured by any other view and is thus visible to the user. |
static Matcher | isEnabled() | 获取 isenabled的View | view.isEnabled() | Returns a matcher that matches Views that are enabled. |
static Matcher | isFocusable() | 获取可以获得焦点的View | view.isFocusable() | Returns a matcher that matches Views that are focusable. |
static Matcher | isJavascriptEnabled() | 获取是WebView及其子类的,并且开启js的View | webView.getSettings().getJavaScriptEnabled() | Returns a matcher that matches WebView if they are evaluating Javascript. |
static Matcher | isNotChecked() | 获取实现了Checkable接口的,并且为被选中的View | withCheckBoxState(is(false)) | Returns a matcher that accepts if and only if the view is a CompoundButton (or subtype of) and is not in checked state. |
static Matcher | isRoot() | 获取是根视图的VIew | view.getRootView().equals(view) | Returns a matcher that matches root View. |
static Matcher | isSelected() | 获取被选中的VIew | view.isSelected() | Returns a matcher that matches Views that are selected. |
static Matcher | supportsInputMethods() | 获取支持输入的View | view.onCreateInputConnection(new EditorInfo()) != null | Returns a matcher that matches views that support input methods. |
static Matcher | withChild(Matcher childMatcher) | 获取具有 “满足childMatcher”作为直接子节点的View | …childMatcher.matches(group.getChildAt(i)) | A matcher that returns true if and only if the view’s child is accepted by the provided matcher. |
static Matcher | withClassName(Matcher classNameMatcher) | 获取具有 满足classNameMatcher的ClassName的View | classNameMatcher.matches(view.getClass().getName()) | Returns a matcher that matches Views with class name matching the given matcher. |
static Matcher | withContentDescription(int resourceId) | 获取其getContentDescription的文本 == resourceId对应的文本的View | … | Returns a Matcher that matches Views based on content description property value. |
static Matcher | withContentDescription(String text) | 获取其getContentDescription的文本 == text的View | withContentDescription(is(text)) | Returns an Matcher that matches Views based on content description property value. |
static Matcher | withContentDescription(Matcher charSequenceMatcher) | 获取其getContentDescription的文本 满足charSequenceMatcher 的View | charSequenceMatcher.matches(view.getContentDescription()) | Returns an Matcher that matches Views based on content description property value. |
static Matcher | withEffectiveVisibility(ViewMatchers.Visibility visibility) | 获取显示在屏幕上的View,也就是其自身和其所有祖父节点都是Visible的 | … | Returns a matcher that matches Views that have “effective” visibility set to the given value. |
static Matcher | withHint(Matcher stringMatcher) | 获取是TextView及其子类类型的,并且其getHint()的文本满足stringMatcher的View | stringMatcher.matches(textView.getHint()) | Returns a matcher that matches TextViews based on hint property value. |
static Matcher | withHint(int resourceId) | 获取是TextView及其子类类型的,并且其getText()的文本 == resourceId对应的文本的View | withCharSequence(resourceId, TextViewMethod.GET_HINT) | Returns a matcher that matches a descendant of TextView that is displaying the hint associated with the given resource id. |
static Matcher | withHint(String hintText) | 获取是TextView及其子类类型的,并且其getHint()的文本 == text的View | withHint(is(text) | Returns a matcher that matches TextView based on it’s hint property value. |
static Matcher | withId(Matcher integerMatcher) | 获取“满足integerMatcher指定的id规则”的View | integerMatcher.matches(view.getId()) | Returns a matcher that matches Views based on resource ids. |
static Matcher | withId(int id) | 获取指定id的view,与withId(is(int))相同,但是尝试寻找对应资源(R.id.xx),如果找到则打印出with id:R.id.xx,如果找不到对应的则打印数字或未找到 | resources = view.getResources();return id == view.getId | (Same as withId(is(int)), but attempts to look up resource name of the given id and use an R.id.myView style description with describeTo. |
static Matcher | withInputType(int inputType) | 获取 是EditText及其子类,并且具有指定输入键盘类型的View | view.getInputType() == inputType | Returns a matcher that matches InputType. |
static Matcher | withParent(Matcher parentMatcher) | 获取 具有“满足parentMatcher”的作为直接父节点的View | parentMatcher.matches(view.getParent()) | A matcher that accepts a view if and only if the view’s parent is accepted by the provided matcher. |
static Matcher | withResourceName(String name) | 获取具有指定资源名的View | withResourceName(is(name) | Returns a matcher that matches Views based on resource id names, (for instance, channel_avatar). |
static Matcher | withResourceName(Matcher stringMatcher) | 获取资源名满足stringMatcher的View | stringMatcher.matches(view.getResources().getResourceEntryName(view.getId())) | Returns a matcher that matches Views based on resource id names, (for instance, channel_avatar). |
static Matcher | withSpinnerText(int resourceId) | 获取是Spinner及其子类类型的,并且其getSelectedItem()的文本 == resourceId对应的文本的View | … | Returns a matcher that matches a descendant of Spinner that is displaying the string of the selected item associated with the given resource id. |
static Matcher | withSpinnerText(String text) | 获取是Spinner及其子类类型的,并且其getSelectedItem()的文本 == text的View | withSpinnerText(is(text) | Returns a matcher that matches Spinner based on it’s selected item’s toString value. |
static Matcher | withSpinnerText(Matcher stringMatcher) | 获取是Spinner及其子类类型的,并且其getSelectedItem()的文本满足stringMatcher的View | stringMatcher.matches(spinner.getSelectedItem().toString()) | |
static Matcher | withTagKey(int key) | 获取 具有指定名称的tag的View | withTagKey(key, Matchers.notNullValue()) | Returns a matcher that matches View based on tag keys. |
static Matcher | withTagKey(int key, Matcher objectMatcher) | 获取 指定名称的tag满足objectMatcher 的View | objectMatcher.matches(view.getTag(key) | Returns a matcher that matches Views based on tag keys. |
static Matcher | withTagValue(Matcher tagValueMatcher) | 获取 其tag满足objectMatcher 的View | tagValueMatcher.matches(view.getTag() | Returns a matcher that matches Views based on tag property values. |
static Matcher | withText(Matcher stringMatcher) | 获取是TextView及其子类类型的,并且其getText()的文本满足stringMatcher的View | stringMatcher.matches(textView.getText().toString()) | Returns a matcher that matches TextViews based on text property value. |
static Matcher | withText(String text) | 获取是TextView及其子类类型的,并且其getText()的文本 == text的View | withText(is(text) | Returns a matcher that matches TextView based on its text property value. |
static Matcher | withText(int resourceId) | 获取是TextView及其子类类型的,并且其getText()的文本 == resourceId对应的文本的View | withCharSequence(resourceId, TextViewMethod.GET_TEXT) | Returns a matcher that matches a descendant of TextView that is displaying the string associated with the given resource id |
2.2 RootMatchers 根视图匹配器的辅助类
匹配root装饰视图匹配给定的视图匹配器,也就是一系列静态方法,返回指定的 Matcher
onView(withText("Text"))
.inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
.perform(click());
RootMatchers还有以下方法可以应用到其他场景:
Tables | Are | Cool |
---|---|---|
isDialog() | 匹配是对话框的根视图 | Matches {@link Root}s that are dialogs (i.e. is not a window of the currently resumed* activity). |
isFocusable() | 匹配可获取焦点的根视图 | Matches {@link Root}s that can take window focus. |
isPlatformPopup() | 匹配弹出式窗体的跟视图,如spinner,actionbar等 | Matches {@link Root}s that are popups - like autocomplete suggestions or the actionbar spinner. |
isTouchable() | 匹配可触摸的根视图 | Matches {@link Root}s that can receive touch events. |
withDecorView(final Matcher decorViewMatcher) | 指定view的跟视图 | Matches {@link Root}s with decor views that match the given view matcher |
下面给几个简单应用
//指定View的跟视图不在当前的Activity根布局层次中,例如Toast
inRoot(withDecorView(not(mRules.getActivity().getWindow().getDecorView())))
inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
2.3 Matchers 对Matcher的操作集合类
- allof(Matchers ) 满足Matchers 定义的所有Matcher
- anyof(Matchers ) 至少满足Matchers 定义的任意一个Matcher
- is(…)
- not(…)
- notNullValue()
- nullValue()
- endsWith(java.lang.String suffix)
- startsWith(java.lang.String prefix)
2.4 CursorMatchers
Hamcrest的集合匹配器,在Cursor匹配相应的数据行
/**
* Returns a matcher that matches a {@link String} value at a given column index
* in a {@link Cursor}s data row.
* <br>
* @param columnIndex int column index
* @param value a {@link String} value to match
*/
public static CursorMatcher withRowString(int columnIndex, String value) {
return withRowString(columnIndex, is(value));
}
大部分的场景,大多发生于表单或者滚动menu时:
onData(
is(instanceOf(Cursor.class)),
CursorMatchers.withRowString("job_title", is("Barista"))
);
2.5 LayoutMatchers 匹配以检测典型的布局问题
例如匹配具有椭圆形文本的TextView元素。
如果文本太长,无法适应TextView,它可以是椭圆形(’Too long’显示为’Too l …’或’… long’)或切断(’Too long“显示为”Too l“)。
虽然在某些情况下可以接受,但通常表示不好的用户体验
2.6 PreferenceMatchers 匹配存储
Preference组件其实就是Android常见UI组件与SharePreferences的组合封装实现
onData(Matchers.<Object>allOf(PreferenceMatchers.withKey("setting-name"))).perform(click());
PreferenceMatchers还有以下方法可以应用到其他场景
withSummary(final int resourceId)
withSummaryText(String summary)
withSummaryText(final Matcher<String> summaryMatcher)
withTitle(final int resourceId)
withTitleText(String title)
withTitleText(final Matcher<String> titleMatcher)
isEnabled()
onView是根据View的相关属性来找到Interaction交互类
OnData则是根据 Data的相关属性来找到Interaction交互类
三、 AdapterView的OnData
ViewInteraction: 关注于已经匹配到的目标控件。通过onView()方法我们可以找到符合匹配条件的唯一的目标控件,我们只需要针对这个控件进行我们需要的操作
- onView()
- Matcher< View>: 构造一个针对于View匹配的匹配规则
DataInteraction: 关注于AdapterView的数据。由于AdapterView的数据源可能很长,很多时候无法一次性将所有数据源显示在屏幕上,因此我们主要先关注AdapterView中包含的数据,而非一次性就进行View的匹配。
- onData
- Matcher< ? extends Object>: 构造一个针对于Object(数据)匹配的匹配规则
onData 只适用于 AdapterView及其派生类,不适用于 RecyleView。
AdapterView是一种通过Adapter来动态加载数据的界面元素。我们常用的ListView, GridView, Spinner等等都属于AdapterView。不同于我们之前提到的静态的控件,AdapterView在加载数据时,可能只有一部分显示在了屏幕上,对于没有显示在屏幕上的那部分数据,我们通过onView()是没有办法找到的。
- Matcher< ? extends Object>的构建规则:
- 类型校验 : 确认AdapterView
- 参数校验:确认item
注:初始化时就显示在屏幕上的adapter中的view你也可以不适用onData()因为他们已经被加载了。然而,使用onDta()会更加安全。
提醒:在打破了继承约束(尤其是getItem()的API)实现了AdatpterView的自定义view中onData()是有问题的。在这中情况下,做好的做法就是重构应用的代码。如果不重构代码,你也可以实现自定义的AdapterViewProtocol来实现。查看Espresso的AdapterViewProtocols 来查看更多信息。
3.1 简单类型
- 类型校验
- 参数校验
onData(allOf(is(instanceOf(String.class)),is("Americano"))).perform(click());
3.2 官方示例
如上 activity 包含一个 ListView,它基于一个为每一行提供一个 Map
onData(allOf(
is(instanceOf(Map.class)),
hasEntry(equalTo("STR"), is("item: 50)
))
.perform(click());
3.2.1 is(instanceOf(Map.class)) 类型校验
限制搜索 AdapterView 中任意条目的条件为一个 Map。
在此例子中,ListView 的所有行都满足条件。但我们想要点击指定的条目 “item: 50”,所以我们需要继续缩小范围:
3.2.2 hasEntry(equalTo(“STR”), is(“item: 50)参数校验
hasEntry(equalTo("STR"), is("item: 50)
优化:
这个 Matcher< String, Object > 会匹配所有包含任意键,值=“item: 50” 的 Map 。鉴于查找此条目的代码较长,而且我们希望在其他地方重用它,我们可以写一个自定义的 matcher “withItemContent”:
public static Matcher<Object> withItemContent(String expectedText) {
checkNotNull(expectedText);
return withItemContent(equalTo(expectedText));
}
public static Matcher<Object> withItemContent(Matcher itemTextMatcher) {
return new BoundedMatcher<Object, Map>(Map.class) {
@Override
public boolean matchesSafely(Map map) {
return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
}
@Override
public void describeTo(Description description) {
description.appendText("with item content: ");
itemTextMatcher.describeTo(description);
}
};
}
现在点击该条目的代码很简单了:
onData(withItemContent("item: 50")).perform(click());
此测试的完整代码请查看 AdapterViewText#testClickOnItem50 和自定义匹配器。
3.3 自定义Adapter:未打破继承约束
使用BoundedMatcher实现:
- 类型校验
- 参数校验
其实没啥说的,直接给个示例吧:
//根据销售机会名称查找到List中的item,并点击
onData(SaleOppMatcher.searchMainItemWithName(oppName)).perform(click());
/**
* 查找指定搜索条件 销售机会匹配
* @param name 需要搜索的字
*/
public static Matcher<Object> searchMainItemWithName(final String name) {
return new BoundedMatcher<Object, SalesOpp>(SalesOpp.class) {
@Override
protected boolean matchesSafely(SalesOpp item) {
return item != null
&& !TextUtils.isEmpty(item.content)
&& item.content.equals(name);
}
@Override
public void describeTo(Description description) {
description.appendText("SalesOpp has Name: " + name);
}
};
}
3.4 自定义Adapter:打破继承约束 usingAdapterViewProtocol
//根据客户拜访名称查找到List中的item,并点击
onData(LegWorkMatcher.searchMainItemWithName(legName)).perform(click());
3.5 OnData支持的筛选
onData我们使用了data进行了筛选,完全屏蔽了View,那么现在问题来了:
假设一个页面里有多个ListView,譬如“ViewPager+ListView的实现”
如果继续用上述方法,会出现:
android.support.test.espresso.AmbiguousViewMatcherException: ‘is assignable from class: class android.widget.AdapterView’ matches multiple views in the hierarchy.
Problem views are marked with ‘*MATCHES*’ below.
大意就是说,有多个AdapterView在界面里,那么结局方案是:
- inAdapterView(Matcher adapterMatcher)
onData(...).inAdapterView(allOf(isAssignableFrom(AdapterView.class),isDisplayed())).perform(click());
onData(...).inAdapterView(allOf(withId(R.id.list),isDisplayed())).perform(click());
现在我们详细的看看DataInteraction还支持那些筛选,来辅助onData:
Tables | 示意 | 适用场景 |
---|---|---|
inAdapterView(Matcher< View > adapterMatcher) | 选择 满足adapterMatcher的adapterview 来执行onData操作 | 当前界面有多个adapterview,导致错误 |
onChildView(Matcher< View> childMatcher) | 匹配”onData所匹配的item视图”中的指定子视图 | 需要点击item中的某个子view |
atPosition(Integer atPosition) | 选中匹配的Adapter的第几项,默认不选择,可以通过atPosition方法设定 | onData只定义“类型校验”, onData(is(instanceOf(String.class)).atPosition(0)… |
inRoot(Matcher< Root> rootMatcher) | 在指定跟视图中来执行onData操作 | 对话框里的adapterview |
- PullToRefreshListView 如何使用inAdapterView?
PullToRefreshListView里面的ListView实际上是嵌套了一个id为android.R.id.list的ListView,因此可以用这个ID来进行匹配。如果单一匹配规则还不够精细,可以再从其他方面构造复合匹配规则。比如针对这种情况我采用了:
onData(allOf(is(instanceOf(TeacherModel.class)),teacherHasName(VALUE_TEACHER_NAME_TARGET)))
.inAdapterView(allOf(withId(android.R.id.list), isDisplayed()))
.perform(click());
3.6 匹配 ListView 的 footer/header 视图
header 和 footer 通过 addHeaderView/addFooterView API 添加到 ListView 中。为了能够使用 Espresso.onData 加载它们,确保使用预置的值来设置数据对象(第二个参数)。
public static final String FOOTER = "FOOTER";
...
View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
((TextView) footerView.findViewById(R.id.item_content)).setText("count:");
((TextView) footerView.findViewById(R.id.item_size)).setText(String.valueOf(data.size()));
listView.addFooterView(footerView, FOOTER, true);
然后,你可以写一个匹配器来匹配此对象:
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
@SuppressWarnings("unchecked")
public static Matcher<Object> isFooter() {
return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
}
在测试中很轻易就能加载该视图:
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.sample.LongListMatchers.isFooter;
public void testClickFooter() {
onData(isFooter())
.perform(click());
...
}
请在 AdapterViewtest#testClickFooter 中查看完整示例代码。
四、RecyleView
RecyclerViews 和 AdapterViews 的工作原理不同,因此onData()不适用于RecyleView
如果想要与RecyleView交互,请引入“espresso-contrib”,里边包含一系列的Actions可以用于滚动和点击
//TODO 根据android.support.test.espresso.contrib.RecyclerViewActions揣摩RecyclerView的macher规则
4.1 actionOnItem源码解析
public static <VH extends ViewHolder> PositionableRecyclerViewAction actionOnItem(
final Matcher<View> itemViewMatcher, final ViewAction viewAction) {
Matcher<VH> viewHolderMatcher = viewHolderMatcher(itemViewMatcher);
return new ActionOnItemViewAction<VH>(viewHolderMatcher, viewAction);
}
4.1.1 viewHolderMatcher
/**
* Creates matcher for view holder with given item view matcher.
*
* @param itemViewMatcher a item view matcher which is used to match item.
* @return a matcher which matches a view holder containing item matching itemViewMatcher.
*/
private static <VH extends ViewHolder> Matcher<VH> viewHolderMatcher(
final Matcher<View> itemViewMatcher) {
return new TypeSafeMatcher<VH>() {
@Override
public boolean matchesSafely(RecyclerView.ViewHolder viewHolder) {
return itemViewMatcher.matches(viewHolder.itemView);
}
@Override
public void describeTo(Description description) {
description.appendText("holder with view: ");
itemViewMatcher.describeTo(description);
}
};
}
4.1.2 ActionOnItemViewAction
private static final class ActionOnItemViewAction<VH extends ViewHolder> implements
PositionableRecyclerViewAction {
private final Matcher<VH> viewHolderMatcher;
private final ViewAction viewAction;
private final int atPosition;
private final ScrollToViewAction<VH> scroller;
private ActionOnItemViewAction(Matcher<VH> viewHolderMatcher, ViewAction viewAction) {
this(viewHolderMatcher, viewAction, NO_POSITION);
}
private ActionOnItemViewAction(Matcher<VH> viewHolderMatcher, ViewAction viewAction,
int atPosition) {
this.viewHolderMatcher = checkNotNull(viewHolderMatcher);
this.viewAction = checkNotNull(viewAction);
this.atPosition = atPosition;
this.scroller = new ScrollToViewAction<VH>(viewHolderMatcher, atPosition);
}
@SuppressWarnings("unchecked")
@Override
public Matcher<View> getConstraints() {
return allOf(isAssignableFrom(RecyclerView.class), isDisplayed());
}
@Override
public PositionableRecyclerViewAction atPosition(int position) {
checkArgument(position >= 0, "%d is used as an index - must be >= 0", position);
return new ActionOnItemViewAction<VH>(viewHolderMatcher, viewAction, position);
}
@Override
public String getDescription() {
if (atPosition == NO_POSITION) {
return String.format("performing ViewAction: %s on item matching: %s",
viewAction.getDescription(), viewHolderMatcher);
} else {
return String.format("performing ViewAction: %s on %d-th item matching: %s",
viewAction.getDescription(), atPosition, viewHolderMatcher);
}
}
@Override
public void perform(UiController uiController, View root) {
RecyclerView recyclerView = (RecyclerView) root;
try {
scroller.perform(uiController, root);
uiController.loopMainThreadUntilIdle();
// the above scroller has checked bounds, dupes (maybe) and brought the element into screen.
int max = atPosition == NO_POSITION ? 2 : atPosition + 1;
int selectIndex = atPosition == NO_POSITION ? 0 : atPosition;
List<MatchedItem> matchedItems = itemsMatching(recyclerView, viewHolderMatcher, max);
actionOnItemAtPosition(matchedItems.get(selectIndex).position, viewAction).perform(
uiController, root);
uiController.loopMainThreadUntilIdle();
} catch (RuntimeException e) {
throw new PerformException.Builder().withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(root)).withCause(e).build();
}
}
}