Open lsuski opened 5 years ago
We are seeing this as well.
The first click on the dialog just doesn't seem to register.
If you put a Thread.Sleep() (hack) just after the dialog is displayed (right before the click), it will work. If you change the click to a doubleClick(), it will also work.
If we run our tests individually, they pass. But if we run the entire class with multiple tests, that is when they become flaky.
I have created a sample project here https://github.com/asavill/esspresso_alert_dialog_issue with an espresso test class which fails 100% of the time when running on Android 10 and passes 100% of the time when running < Android 10.
Thank you to @lsuski for providing the other sample application.
Run ExampleInstrumentedTest
ExampleInstrumentedTest should pass
ExampleInstrumentedTest fails on API 29 and passes on older API versions.
AndroidX Test and Android OS Versions (have also tested on latest alpha versions as of 16/11/19)
espresso-core:3.1.1 androidx.test.ext:junit:1.1.0 androidx.appcompat:appcompat:1.1.1
Android 10 - api 29
Can you reproduce the issue with animations disabled? Its strongly recommended to disable animations when using espresso. https://developer.android.com/training/testing/espresso/setup
Can you reproduce the issue with animations disabled? Its strongly recommended to disable animations when using espresso. https://developer.android.com/training/testing/espresso/setup
Hello, yes animations on or off does not make a difference.
Thanks we'll take a look
Loved the way you closed the issue without waiting for a response. And then haven't updated anything for like 4 months now?
This is android 10 issue. It happens in couple of devices with 10 we have for testing. Even after updating Nokia7.2 to android 10 this bug starts to show up.
Will this be fixed ? I'm also encountering this issue.
I confirm this happens on Samsung S10.
For what it's worth, I can confirm this happens on the SDK 29 emulator (with about a 50% failure rate for me) but not on the SDK 24 emulator. androidx.test 1.3.0 (Espresso 3.3.0)
Also encountering this. Have not managed to find a robust solution other than adding a Thread.sleep
. Waiting on visibility, clickablility etc doesn't help and putting in "try again on failure" style logic can cause problems. It feels like using a custom on click listener for dialogs that Espresso can inspect might be a workaround for whatever the real problem is but that'll be pretty invasive.
I have also struggled with this bug and can add the observation, that when the failure occurs, the click seems to miss the dialogue, i.e. it is executed, but the event gets routed towards the activity window behind the dialog.
@brettchabot is this being looked at all? This seems to be forcing people to (I definitely have had to) add Thread.sleep
to their tests to get something reliable.
@adazh any thoughts?
Can you try using our latest Espresso release Espresso 3.5.0-alpha04?
Not sure if it could fix this issue, but it contains a fix that uses consistent input device source for event injection, which may be something relevant.
I've run our suite a few times with 3.5.0-alpha04
against an API 29 and API 30 Pixel 2 on Firebase (without mitigation code disabled) and it does indeed seem to be more stable! It'd be interesting to see if others get similar results. I can investigate again once there is stable release of 3.5.0
.
Nice! Thanks for confirming!
I am still seeing the problem after running on Espresso 3.5.0.
@dabrosch Can you also try with 3.5.0-alpha04
? If the problem persists, please also upload a sample that we could repro the issue. Thanks.
I will have to extract out non-proprietary code to publish that, and I am not 100% sure I can repro it because the entire application is huge, and this failure is definitely related to a race condition, which means it is "touchy." But what I can 100% say is that without a 250 msec wait hack placed immediately after a popup is found to exist (via UiAutomator), the espresso click attempt fails 95% of the time.
FWIW I did some testing using the sample posted earlier: https://github.com/asavill/esspresso_alert_dialog_issue
Interestingly this sample (with espresso 3.1.1) can repro the problem 100% on an API 29 emulator, but the test passes reliably on every other API level (24, 28, 30,31,33) I've tried.
Using the latest stable release of all dependencies, including espresso (3.5.0) helps a little but the failure rate still seems > 50%). I tried adding logging and investigating more but didn't find anything yet
I've created few workarounds in our internal library for dialog clicking:
The other issue I faced rarely was due to View not being in touch mode. I faced this with sending KEYCODE_BACK as first event in a test. In such case motion down event is ignored
I can try the other versions though.
@lsuski do you mind providing more details how you are monitoring for dialog enter animation? I know Instrumentation.startActivitySync has logic to wait for activity animations to complete (which ActivityScenario now uses), but that logic doesn't seem possible to do as an app.
I would hope disabling animations would prevent dialog and activity animations from occurring in the first place but I suspect that might not be the case.
This line of code also in Instrumentation.startActivitySync seems potentially relevant: https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Instrumentation.java;drc=0f9ea02be099947968936258e1f0746a43470a7c;l=568
Unfortunately I don't see a way of calling that outside of the android framework either on API 29. API 33 introduces a TransactionCommittedListener but that doesn't help here...
Another workaround that appeared to work on @asavill sample is to force a redraw on the dialog. Basically this logic here used for androidx.test screenshots .
I'll share it on Monday. The code you mentioned is for Activity only started by test but during the test other activities can be started by app and this logic does not apply. Also afaik dialog has its own enter animation.
private const val AWAIT_TIME = 120L
override fun perform(uiController: UiController, view: View) {
val rootView = view.rootView
if (lastRootView?.get() != rootView) {
lastRootView = rootView.toWeakRef()
val waitTime = rootView.getAnimationTime() ?: AWAIT_TIME
if (waitTime > 0) {
val finalWait = max(waitTime, AWAIT_TIME)
Log.i("RootAwaitAction", "loopMainThreadForAtLeast $finalWait")
uiController.loopMainThreadForAtLeast(finalWait)
}
}
}
private fun View.getAnimationTime(): Long? {
val layoutParams = EspressoExposer.listActiveRoots().first { it.decorView.rootView == this }.windowLayoutParams.get()
if (layoutParams.windowAnimations != 0) {
val array = context.obtainStyledAttributes(layoutParams.windowAnimations, intArrayOf(android.R.attr.windowEnterAnimation))
val resourceId = array.getResourceId(0, -1)
array.recycle()
if (resourceId != -1) {
val animDuration = AnimationUtils.loadAnimation(context, resourceId).duration
val waitForAnimation = SystemClock.uptimeMillis() - drawingTime - animDuration
return if (waitForAnimation < 0) {
-waitForAnimation
} else 0
}
}
return null
}
This basically detects if this is new root than previous one and waits for 120ms or some remaining time calculated from enter animation duration
Thanks for the details, interesting approach. Correct me if I'm wrong but that solution appears to be essentially a conditional sleep, set to the value of animation duration.
I realize the limitations of the solutions I posted earlier, but unfortunately our options to work around this particular issue seem limited.
@brettchabot yes, you're right
My workaround is to use the ConditionWatcher
with a custom ViewInstruction
that waits for a certain Espresso view condition to be met.
Basically is a loop checking for a condition every X ms with a Thread.sleep
.
This is the custom ViewInstruction
for ConditionWatcher
.
class ViewInstruction(
private val viewMatcher: Matcher<View>,
private val idleMatcher: Matcher<View>,
private val rootMatcher: Matcher<Root> = RootMatchers.DEFAULT,
) : Instruction() {
private var assertionThrowable: Throwable? = null
override fun getDescription(): String {
val stringBuilder = StringBuilder()
.append(toString())
.append(" ")
.append(viewMatcher.description())
assertionThrowable?.let { assertionThrowable ->
stringBuilder
.append(" ")
.append("(${assertionThrowable.message ?: "$assertionThrowable"})")
}
val viewException = getExceptionWithViewHierarchy()
stringBuilder
.append("\nView hierarchy:\n")
.append(viewException.stackTraceToString())
return stringBuilder.toString()
}
override fun checkCondition(): Boolean =
idleMatcher.matches(
getView(viewMatcher),
)
private fun getView(
viewMatcher: Matcher<View>,
): View? =
try {
var view: View? = null
runOnUiThread {
val viewInteraction = onView(viewMatcher).inRoot(rootMatcher)
val finderField: Field = viewInteraction
.javaClass
.getDeclaredField("viewFinder").apply {
isAccessible = true
}
val finder = finderField.get(viewInteraction) as ViewFinder
view = finder.view
}
view
} catch (e: Throwable) {
assertionThrowable = e
null
}
private fun getExceptionWithViewHierarchy(): Throwable =
runCatching {
assertDisplayed("Force non existent text to print hierarchy")
}.exceptionOrNull()!!
}
These are some helper methods to wait for view conditions:
fun waitUntilViewIsDisplayedInDialog(text: String) {
waitUntilViewMatchesCondition(text, isDisplayed())
}
fun waitUntilViewIsDisplayedInDialog(viewStringId: Int) {
waitUntilViewMatchesCondition(viewStringId, isDisplayed())
}
private fun waitUntilViewMatchesCondition(viewStringId: Int, condition: Matcher<View>) {
ConditionWatcher.waitForCondition(
ViewInstruction(
viewStringId.resourceMatcher(),
condition,
isDialog(),
),
)
}
private fun waitUntilViewMatchesCondition(text: String, condition: Matcher<View>) {
ConditionWatcher.waitForCondition(
ViewInstruction(
withText(text),
condition,
isDialog(),
),
)
}
And this is how I use it with the robot pattern:
fun someDialog(
func: SomeDialogRobot.() -> Unit,
): SomeDialogRobot {
waitUntilViewIsDisplayedInDialog(R.string.some_string_on_the_dialog)
return SomeDialogRobot().apply(func)
}
class SomeDialogRobot
That solves the dialog click 100% of the time in my tests.
Description
On Pixel 3 (this is probably android 10 specific issue but I've tested it only on Pixel 3) simple clicking on dialog button immediately after it is shown does not work
Steps to Reproduce
Animations are enabled on device.
Run ExampleInstrumentedTest
Expected Results
ExampleInstrumentedTest should pass
Actual Results
ExampleInstrumentedTest fails on Pixel 3 and passes on older devices
AndroidX Test and Android OS Versions
espresso-core:3.2.0 androidx.test.ext:junit:1.1.1 androidx.appcompat:appcompat:1.0.2
Android 10 - api 29
Link to a public git repo demonstrating the problem:
https://github.com/lsuski/espresso-android10-bug-sample