Espresso doesn’t wait for swipe action on a ViewPager to be finished

Issue

Espresso is advertised with the feature that it always waits for the UI-Thread of Android to be idle so that you don’t have to take care of any timing issues. But I seem to have found an exception :-/

The setting is a ViewPager with an EditText in each fragment. I want Espresso to type text into theEditText on the first fragment, swipe to the second fragment and do the same with the EditText in that fragment (3 times):

@MediumTest
public void testSwipe() throws InterruptedException {
    onView(withIdInActiveFragment(EXTERN_HOURS_INPUT))
            .perform(typeText("8.0"));
    onView(withIdInActiveFragment(DAY_PAGER))
            .perform(swipeLeft());
    //Thread.sleep(2000); // <--- uncomment this and the test runs fine
    onView(withIdInActiveFragment(EXTERN_HOURS_INPUT))
            .perform(typeText("8.0"));
    onView(withIdInActiveFragment(DAY_PAGER))
            .perform(swipeLeft());
    //Thread.sleep(2000);
    onView(withIdInActiveFragment(EXTERN_HOURS_INPUT))
            .perform(typeText("8.0"));
    onView(withIdInActiveFragment(DAY_PAGER))
            .perform(swipeLeft());
}

public static Matcher<View> withIdInActiveFragment(int id) {
    return Matchers.allOf(withParent(isDisplayed()), withId(id));
}

But I get this error while performing the first swipe:

android.support.test.espresso.AmbiguousViewMatcherException: '(has parent matching: is displayed on the screen to the user and with id: de.cp.cp_app_android:id/extern_hours_input)' matches multiple views in the hierarchy.
Problem views are marked with '****MATCHES****' below.

View Hierarchy:
...

+-------->AppCompatEditText{id2131558508, res-nameextern_hours_input, visibilityVISIBLE, width110, height91, has-focustrue, has-focusabletrue, has-window-focustrue, is-clickabletrue, is-enabledtrue, is-focusedtrue, is-focusabletrue, is-layout-requestedfalse, is-selectedfalse, root-is-layout-requestedfalse, has-input-connectiontrue, editor-info[inputType0x2002 imeOptions0x6 privateImeOptionsnull actionLabelnull actionId0 initialSelStart3 initialSelEnd3 initialCapsMode0x0 hintTextnull labelnull packageNamenull fieldId0 fieldNamenull extrasnull ], x165.0, y172.0, text8.0, input-type8194, ime-targettrue, has-linksfalse} ****MATCHES****

...  


+-------->AppCompatEditText{id2131558508, res-nameextern_hours_input, visibilityVISIBLE, width110, height91, has-focusfalse, has-focusabletrue, has-window-focustrue, is-clickabletrue, is-enabledtrue, is-focusedfalse, is-focusabletrue, is-layout-requestedfalse, is-selectedfalse, root-is-layout-requestedfalse, has-input-connectiontrue, editor-info[inputType0x2002 imeOptions0x6 privateImeOptionsnull actionLabelnull actionId0 initialSelStart0 initialSelEnd0 initialCapsMode0x2000 hintTextnull labelnull packageNamenull fieldId0 fieldNamenull extrasnull ], x165.0, y172.0, text, input-type8194, ime-targetfalse, has-linksfalse} ****MATCHES****

Espresso wants to write into an EditText with the ID EXTERN_HOURS_INPUT that is visible. Because the swipe action is not finished yet, both the EditTexts in the first and the second fragment are visible, wich is why the matching onView(withIdInActiveFragment(EXTERN_HOURS_INPUT)) fails with 2 matches.

If I manually force a break by adding Thread.sleep(2000); after the swipe action, everything is fine.

Does anybody know how to make Espresso wait until the swipe action is done? Or does anybody at least know, why this happens? Because the UI-Thread can’t be idle when there is a swipe action performed, can he?

Here is the activity_day_time_record.xml

<layout xmlns:android"http://schemas.android.com/apk/res/android"
xmlns:bind"http://schemas.android.com/apk/res-auto">

<data>
    <variable
        name"timerecord"
        type"de.cp.cp_app_android.model.TimerecordDatabindingWrapper" />
</data>

<ScrollView
    android:layout_width"wrap_content"
    android:layout_height"wrap_content">

    <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"
        xmlns:tools"http://schemas.android.com/tools"
        style"@style/cp_relative_layout"
        android:descendantFocusability"afterDescendants"
        tools:context".activities.DayRecordActivity">

        <include
            android:id"@+id/toolbar"
            layout"@layout/cp_toolbar"></include>


        <!-- Arbeitsstunden -->
        <TextView xmlns:android"http://schemas.android.com/apk/res/android"
            android:id"@+id/section_title_workhours"
            style"?android:attr/listSeparatorTextViewStyle"
            android:layout_width"match_parent"
            android:layout_height"25dip"
            android:layout_below"@id/toolbar"
            android:text"@string/dayrecord_section_workhours" />

        <TextView
            android:id"@+id/extern_hours"
            style"@style/dayrecord_label"
            android:layout_below"@id/section_title_workhours"
            android:text"@string/dayrecord_label_extern_hours" />

        <EditText
            android:id"@+id/extern_hours_input"
            style"@style/dayrecord_decimal_input"
            android:layout_alignBaseline"@id/extern_hours"
            android:layout_toEndOf"@id/extern_hours"
            android:layout_toRightOf"@id/extern_hours"
            bind:addTextChangedListener"@{timerecord.changed}"
            bind:binding"@{timerecord.hoursExtern}"
            bind:setOnFocusChangeListener"@{timerecord.hoursExternChanged}" />
        <!--    android:text'@{timerecord.hoursExtern ! null ? String.format("%.1f", timerecord.hoursExtern) : ""}' -->


    </RelativeLayout>
</ScrollView>

And the activity_swipe_day.xml:

<android.support.v4.view.ViewPager xmlns:android"http://schemas.android.com/apk/res/android"
android:id"@+id/day_pager"
android:layout_width"match_parent"
android:layout_height"match_parent" >

Solution

I don’t think Espresso has anything built in with this functionality, they want you to use idling-resource. The best I’ve come up with is still using sleep but polls every 100 milliseconds, so at worst will return 100 ms after the view becomes visible.

private final int TIMEOUT_MILLISECONDS  5000;
private final int SLEEP_MILLISECONDS  100;
private int time  0;
private boolean wasDisplayed  false;

public Boolean isVisible(ViewInteraction interaction) throws InterruptedException {
    interaction.withFailureHandler((error, viewMatcher) -> wasDisplayed  false);
    if (wasDisplayed) {
        time  0;
        wasDisplayed  false;
        return true;
    }
    if (time > TIMEOUT_MILLISECONDS) {
        time  0;
        wasDisplayed  false;
        return false;
    }

    //set it to true if failing handle should set it to false again.
    wasDisplayed  true;
    Thread.sleep(SLEEP_MILLISECONDS);
    time + SLEEP_MILLISECONDS;

    interaction.check(matches(isDisplayed()));
    Log.i("ViewChecker","sleeping");
    return isVisible(interaction);
}

You can then call it like this:

ViewInteraction interaction  onView(
        allOf(withId(R.id.someId), withText(someText), isDisplayed()));
boolean objectIsVisible  isVisible(interaction);
assertThat(objectIsVisible, is(true));

Answered By – Kai

Leave a Comment