You can try to create a custom wait action with both IdlingResource
and ViewAction
. The simplest way is to create an IdlingResource
callback that listens to ViewTreeObserver.OnDrawListener
and determines when the app is idling by matching with a Matcher<View>
:
private class ViewPropertyChangeCallback(private val matcher: Matcher<View>, private val view: View) : IdlingResource, ViewTreeObserver.OnDrawListener {
private lateinit var callback: IdlingResource.ResourceCallback
private var matched = false
override fun getName() = "View property change callback"
override fun isIdleNow() = matched
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}
override fun onDraw() {
matched = matcher.matches(view)
callback.onTransitionToIdle()
}
}
Then create a custom ViewAction
to wait for a match:
fun waitUntil(matcher: Matcher<View>): ViewAction = object : ViewAction {
override fun getConstraints(): Matcher<View> {
return any(View::class.java)
}
override fun getDescription(): String {
return StringDescription().let {
matcher.describeTo(it)
"wait until: $it"
}
}
override fun perform(uiController: UiController, view: View) {
if (!matcher.matches(view)) {
ViewPropertyChangeCallback(matcher, view).run {
try {
IdlingRegistry.getInstance().register(this)
view.viewTreeObserver.addOnDrawListener(this)
uiController.loopMainThreadUntilIdle()
} finally {
view.viewTreeObserver.removeOnDrawListener(this)
IdlingRegistry.getInstance().unregister(this)
}
}
}
}
}
And perform this action on a root view:
fun waitForElement(matcher: Matcher<View>) {
onView(isRoot()).perform(waitUntil(hasDescendant(matcher))
}
...
waitForElement(allOf(withId(R.id.elementID), isDisplayed()))
Or if the view's property can change asynchronously, you can do:
onView(withId(R.id.elementID)).perform(waitUntil(withText("textChanged!")))
Also, this action may not be suitable for activity transitions.