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

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{id=2131558508, res-name=extern_hours_input, visibility=VISIBLE, width=110, height=91, has-focus=true, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=true, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=true, editor-info=[inputType=0x2002 imeOptions=0x6 privateImeOptions=null actionLabel=null actionId=0 initialSelStart=3 initialSelEnd=3 initialCapsMode=0x0 hintText=null label=null packageName=null fieldId=0 fieldName=null extras=null ], x=165.0, y=172.0, text=8.0, input-type=8194, ime-target=true, has-links=false} ****MATCHES****

...  


+-------->AppCompatEditText{id=2131558508, res-name=extern_hours_input, visibility=VISIBLE, width=110, height=91, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=true, is-enabled=true, is-focused=false, is-focusable=true, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=true, editor-info=[inputType=0x2002 imeOptions=0x6 privateImeOptions=null actionLabel=null actionId=0 initialSelStart=0 initialSelEnd=0 initialCapsMode=0x2000 hintText=null label=null packageName=null fieldId=0 fieldName=null extras=null ], x=165.0, y=172.0, text=, input-type=8194, ime-target=false, has-links=false} ****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 1:

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));

Solution 2:

I expand Kai answer so it suits kotlin/coroutine better

    suspend fun ViewInteraction.waitTillViewIsDisplayed(
    timeout: Int = 3_000,
    suspensionPeriod: Int = 100
    ): ViewInteraction {
    var time = 0
    var wasDisplayed = false
    while (time < timeout) {
        this.withFailureHandler { _: Throwable?, _: Matcher<View?>? ->
            wasDisplayed = false
        }
        this.check(matches(isDisplayed()))
        if (wasDisplayed) {
            return this
        }
        //set it to true if failing handle should set it to false again.
        wasDisplayed = true
        delay(suspensionPeriod.toLong())
        time += suspensionPeriod
        Log.i("isVisible: ViewChecker", "Thread slept for $time milliseconds")
    }
    //after timeOut this will throw isDisplayed exception if view is not still visible
    val additionalErrorInfo = "ViewInteraction.waitTillViewIsDisplayed: We just wait for $this " +
            "to display for $timeout milliseconds but it did not \n moreInfo: "
    this.withFailureHandler { error: Throwable?, _: Matcher<View?>? ->
        throw Throwable(message = additionalErrorInfo + error?.message, error)
    }
    this.check(matches(isDisplayed()))
    return this

}

You can call it like this:

onView(allOf(withId(R.id.someId), withText(someText), isDisplayed())).waitTillViewIsDisplayed().perform(click())