Espresso 不会等待 ViewPager 上的滑动操作完成
Espresso doesn't wait for swipe action on a ViewPager to be finished
Espresso 的广告功能是它始终等待 Android 的 UI-线程空闲,这样您就不必处理任何时间问题。但是我好像发现了一个例外:-/
设置是 ViewPager
,每个片段中有一个 EditText
。我希望 Espresso 在第一个片段的 EditText
中输入文本,滑动到第二个片段,然后对该片段中的 EditText
执行相同的操作(3 次):
@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));
}
但是我在执行第一次滑动时收到此错误:
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 想要写入 ID EXTERN_HOURS_INPUT
可见的 EditText
。由于滑动操作尚未完成,第一个和第二个片段中的 EditTexts
都可见,这就是匹配 onView(withIdInActiveFragment(EXTERN_HOURS_INPUT))
失败并出现 2 个匹配的原因。
如果我通过在滑动操作后添加 Thread.sleep(2000);
手动强制中断,一切都很好。
有人知道如何让 Espresso 等待滑动动作完成吗?或者至少有人知道,为什么会这样?因为UI-线程在执行滑动动作时不能空闲,他可以吗?
这是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>
和 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" >
我认为 Espresso 没有内置此功能,他们希望您使用 idling-resource。
我想出的最好的方法仍然是使用睡眠,但每 100 毫秒轮询一次,所以最坏的情况是视图变为可见后 return 100 毫秒。
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);
}
然后你可以这样称呼它:
ViewInteraction interaction = onView(
allOf(withId(R.id.someId), withText(someText), isDisplayed()));
boolean objectIsVisible = isVisible(interaction);
assertThat(objectIsVisible, is(true));
我扩展了 Kai 的答案,使其更适合 kotlin/coroutine
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
}
你可以这样称呼它:
onView(allOf(withId(R.id.someId), withText(someText), isDisplayed())).waitTillViewIsDisplayed().perform(click())
Espresso 的广告功能是它始终等待 Android 的 UI-线程空闲,这样您就不必处理任何时间问题。但是我好像发现了一个例外:-/
设置是 ViewPager
,每个片段中有一个 EditText
。我希望 Espresso 在第一个片段的 EditText
中输入文本,滑动到第二个片段,然后对该片段中的 EditText
执行相同的操作(3 次):
@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));
}
但是我在执行第一次滑动时收到此错误:
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 想要写入 ID EXTERN_HOURS_INPUT
可见的 EditText
。由于滑动操作尚未完成,第一个和第二个片段中的 EditTexts
都可见,这就是匹配 onView(withIdInActiveFragment(EXTERN_HOURS_INPUT))
失败并出现 2 个匹配的原因。
如果我通过在滑动操作后添加 Thread.sleep(2000);
手动强制中断,一切都很好。
有人知道如何让 Espresso 等待滑动动作完成吗?或者至少有人知道,为什么会这样?因为UI-线程在执行滑动动作时不能空闲,他可以吗?
这是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>
和 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" >
我认为 Espresso 没有内置此功能,他们希望您使用 idling-resource。 我想出的最好的方法仍然是使用睡眠,但每 100 毫秒轮询一次,所以最坏的情况是视图变为可见后 return 100 毫秒。
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);
}
然后你可以这样称呼它:
ViewInteraction interaction = onView(
allOf(withId(R.id.someId), withText(someText), isDisplayed()));
boolean objectIsVisible = isVisible(interaction);
assertThat(objectIsVisible, is(true));
我扩展了 Kai 的答案,使其更适合 kotlin/coroutine
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
}
你可以这样称呼它:
onView(allOf(withId(R.id.someId), withText(someText), isDisplayed())).waitTillViewIsDisplayed().perform(click())