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{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
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
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.