(4.5.5.4)Espresso的进阶: OnView & onData & Matchers

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 voidassertThat(String message, T actual, Matcher matcher)A replacement for MatcherAssert.assertThat that renders View objects nicely.
static voidassertThat(T actual, Matcher matcher)A replacement for MatcherAssert.assertThat that renders View objects nicely.
static MatcherhasContentDescription()获取具有ContentDescription的viewview.getContentDescription() != nullReturns an Matcher that matches Views with any content description.
static MatcherhasDescendant(Matcher descendantMatcher)获取具有“descendantMatcher满足的View”作为直接或间接子节点的VIewReturns a matcher that matches Views based on the presence of a descendant in its view hierarchy.
static MatcherhasErrorText(String expectedError)获取是 EditText 及其子类类型的,并且getError()== expectedError 的ViewhasErrorText(is(expectedError))Returns a matcher that matches EditText based on edit text error string value.
static MatcherhasErrorText(Matcher stringMatcher)获取是 EditText 及其子类类型的,并且getError()满足stringMatcher的ViewstringMatcher.matches(view.getError().toString())Returns a matcher that matches EditText based on edit text error string value.
static MatcherhasFocus()获取持有焦点的Viewview.hasFocus()Returns a matcher that matches Views currently have focus.
static MatcherhasImeAction(int imeAction)支持输入的,并且具有指定imeAction的ViewReturns a matcher that matches views that support input methods (e.g. EditText) and have the* specified IME action set in its {@link EditorInfo}
static MatcherhasImeAction(Matcher imeActionMatcher)支持输入的,并且满足imeActionMatcher的ViewReturns a matcher that matches views that support input methods (e.g…
static MatcherhasLinks()获取是TextView及其子类类型的,并且具有出超链接的ViewtextView.getUrls().length > 0Returns a matcher that matches TextViews that have links.
static MatcherhasSibling(Matcher siblingMatcher)获取 在View层级上具有“满足siblingMatcher”作为直接或间接兄弟的ViewsiblingMatcher.matches(parentGroup.getChildAt(i))Returns an Matcher that matches Views based on their siblings.
static MatcherisAssignableFrom(Class clazz)获取 是clazz子类的Viewclazz.isAssignableFrom(view.getClass())Returns a matcher that matches Views which are an instance of or subclass of the provided class.
static MatcherisChecked()获取实现了Checkable接口的,并且被选中的ViewwithCheckBoxState(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 MatcherisClickable()获取可以点击的Viewview.isClickable()Returns a matcher that matches Views that are clickable.
static MatcherisCompletelyDisplayed()获取正在完全显示的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 MatcherisDescendantOfA(Matcher ancestorMatcher)获取 拥有”满足ancestorMatcher的view”的作为父辈节点的ViewcheckAncestors(ViewParent viewParent, Matcher ancestorMatcher)Returns a matcher that matches Views based on the given ancestor type.
static MatcherisDisplayed()获取正在显示的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 MatcherisDisplayingAtLeast(int areaPercentage)获取至少有areaPercentage百分比在显示的ViewReturns 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 MatcherisEnabled()获取 isenabled的Viewview.isEnabled()Returns a matcher that matches Views that are enabled.
static MatcherisFocusable()获取可以获得焦点的Viewview.isFocusable()Returns a matcher that matches Views that are focusable.
static MatcherisJavascriptEnabled()获取是WebView及其子类的,并且开启js的ViewwebView.getSettings().getJavaScriptEnabled()Returns a matcher that matches WebView if they are evaluating Javascript.
static MatcherisNotChecked()获取实现了Checkable接口的,并且为被选中的ViewwithCheckBoxState(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 MatcherisRoot()获取是根视图的VIewview.getRootView().equals(view)Returns a matcher that matches root View.
static MatcherisSelected()获取被选中的VIewview.isSelected()Returns a matcher that matches Views that are selected.
static MatchersupportsInputMethods()获取支持输入的Viewview.onCreateInputConnection(new EditorInfo()) != nullReturns a matcher that matches views that support input methods.
static MatcherwithChild(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 MatcherwithClassName(Matcher classNameMatcher)获取具有 满足classNameMatcher的ClassName的ViewclassNameMatcher.matches(view.getClass().getName())Returns a matcher that matches Views with class name matching the given matcher.
static MatcherwithContentDescription(int resourceId)获取其getContentDescription的文本 == resourceId对应的文本的ViewReturns a Matcher that matches Views based on content description property value.
static MatcherwithContentDescription(String text)获取其getContentDescription的文本 == text的ViewwithContentDescription(is(text))Returns an Matcher that matches Views based on content description property value.
static MatcherwithContentDescription(Matcher charSequenceMatcher)获取其getContentDescription的文本 满足charSequenceMatcher 的ViewcharSequenceMatcher.matches(view.getContentDescription())Returns an Matcher that matches Views based on content description property value.
static MatcherwithEffectiveVisibility(ViewMatchers.Visibility visibility)获取显示在屏幕上的View,也就是其自身和其所有祖父节点都是Visible的Returns a matcher that matches Views that have “effective” visibility set to the given value.
static MatcherwithHint(Matcher stringMatcher)获取是TextView及其子类类型的,并且其getHint()的文本满足stringMatcher的ViewstringMatcher.matches(textView.getHint())Returns a matcher that matches TextViews based on hint property value.
static MatcherwithHint(int resourceId)获取是TextView及其子类类型的,并且其getText()的文本 == resourceId对应的文本的ViewwithCharSequence(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 MatcherwithHint(String hintText)获取是TextView及其子类类型的,并且其getHint()的文本 == text的ViewwithHint(is(text)Returns a matcher that matches TextView based on it’s hint property value.
static MatcherwithId(Matcher integerMatcher)获取“满足integerMatcher指定的id规则”的ViewintegerMatcher.matches(view.getId())Returns a matcher that matches Views based on resource ids.
static MatcherwithId(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 MatcherwithInputType(int inputType)获取 是EditText及其子类,并且具有指定输入键盘类型的Viewview.getInputType() == inputTypeReturns a matcher that matches InputType.
static MatcherwithParent(Matcher parentMatcher)获取 具有“满足parentMatcher”的作为直接父节点的ViewparentMatcher.matches(view.getParent())A matcher that accepts a view if and only if the view’s parent is accepted by the provided matcher.
static MatcherwithResourceName(String name)获取具有指定资源名的ViewwithResourceName(is(name)Returns a matcher that matches Views based on resource id names, (for instance, channel_avatar).
static MatcherwithResourceName(Matcher stringMatcher)获取资源名满足stringMatcher的ViewstringMatcher.matches(view.getResources().getResourceEntryName(view.getId()))Returns a matcher that matches Views based on resource id names, (for instance, channel_avatar).
static MatcherwithSpinnerText(int resourceId)获取是Spinner及其子类类型的,并且其getSelectedItem()的文本 == resourceId对应的文本的ViewReturns a matcher that matches a descendant of Spinner that is displaying the string of the selected item associated with the given resource id.
static MatcherwithSpinnerText(String text)获取是Spinner及其子类类型的,并且其getSelectedItem()的文本 == text的ViewwithSpinnerText(is(text)Returns a matcher that matches Spinner based on it’s selected item’s toString value.
static MatcherwithSpinnerText(Matcher stringMatcher)获取是Spinner及其子类类型的,并且其getSelectedItem()的文本满足stringMatcher的ViewstringMatcher.matches(spinner.getSelectedItem().toString())
static MatcherwithTagKey(int key)获取 具有指定名称的tag的ViewwithTagKey(key, Matchers.notNullValue())Returns a matcher that matches View based on tag keys.
static MatcherwithTagKey(int key, Matcher objectMatcher)获取 指定名称的tag满足objectMatcher 的ViewobjectMatcher.matches(view.getTag(key)Returns a matcher that matches Views based on tag keys.
static MatcherwithTagValue(Matcher tagValueMatcher)获取 其tag满足objectMatcher 的ViewtagValueMatcher.matches(view.getTag()Returns a matcher that matches Views based on tag property values.
static MatcherwithText(Matcher stringMatcher)获取是TextView及其子类类型的,并且其getText()的文本满足stringMatcher的ViewstringMatcher.matches(textView.getText().toString())Returns a matcher that matches TextViews based on text property value.
static MatcherwithText(String text)获取是TextView及其子类类型的,并且其getText()的文本 == text的ViewwithText(is(text)Returns a matcher that matches TextView based on its text property value.
static MatcherwithText(int resourceId)获取是TextView及其子类类型的,并且其getText()的文本 == resourceId对应的文本的ViewwithCharSequence(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还有以下方法可以应用到其他场景:

TablesAreCool
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();
      }
    }
  }

参考文献

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值