Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dialog click not working on Pixel 3 (Android 10) #444

Open
lsuski opened this issue Oct 7, 2019 · 30 comments
Open

Dialog click not working on Pixel 3 (Android 10) #444

lsuski opened this issue Oct 7, 2019 · 30 comments

Comments

@lsuski
Copy link

lsuski commented Oct 7, 2019

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

  1. Animations are enabled on device.

  2. 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

@asavill
Copy link

asavill commented Nov 16, 2019

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.

Steps to Reproduce

Run ExampleInstrumentedTest

Expected Results

ExampleInstrumentedTest should pass

Actual Results

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

@brettchabot
Copy link
Collaborator

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

@asavill
Copy link

asavill commented Jan 31, 2020

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.

@brettchabot brettchabot reopened this Jan 31, 2020
@brettchabot
Copy link
Collaborator

Thanks we'll take a look

@ShivamPokhriyal
Copy link

Loved the way you closed the issue without waiting for a response. And then haven't updated anything for like 4 months now?

@lsuski
Copy link
Author

lsuski commented May 24, 2020

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.

@rh-id
Copy link

rh-id commented Aug 23, 2020

Will this be fixed ? I'm also encountering this issue.

@diegoperini
Copy link

I confirm this happens on Samsung S10.

@tauntz
Copy link

tauntz commented Feb 23, 2021

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)

nenick added a commit to nenick/espresso-macchiato that referenced this issue Apr 23, 2021
nenick added a commit to nenick/espresso-macchiato that referenced this issue Apr 23, 2021
@seadowg
Copy link

seadowg commented Nov 16, 2021

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.

@mtotschnig
Copy link

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.

@seadowg
Copy link

seadowg commented Feb 23, 2022

@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.

@brettchabot
Copy link
Collaborator

@adazh any thoughts?

@adazh
Copy link
Collaborator

adazh commented Mar 1, 2022

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.

@seadowg
Copy link

seadowg commented Mar 2, 2022

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.

@adazh
Copy link
Collaborator

adazh commented Mar 2, 2022

Nice! Thanks for confirming!

@dabrosch
Copy link

I am still seeing the problem after running on Espresso 3.5.0.

@adazh
Copy link
Collaborator

adazh commented Nov 30, 2022

@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.

@dabrosch
Copy link

dabrosch commented Dec 1, 2022

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.

@brettchabot
Copy link
Collaborator

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

@lsuski
Copy link
Author

lsuski commented Dec 1, 2022

I've created few workarounds in our internal library for dialog clicking:

  • postpone clicking after enter animation finishes which imho is the main cause of this issue. When click happens during dialog/activity enter animation it is swallowed
  • postpone clicking after dialog button stops drawing which is checked in 50ms interval - this prevents misclicks caused by dialog jumps during keyboard hiding

@lsuski
Copy link
Author

lsuski commented Dec 1, 2022

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

@dabrosch
Copy link

dabrosch commented Dec 2, 2022

I can try the other versions though.

@brettchabot
Copy link
Collaborator

@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.

@brettchabot
Copy link
Collaborator

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 .

@lsuski
Copy link
Author

lsuski commented Dec 3, 2022

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.

@lsuski
Copy link
Author

lsuski commented Dec 5, 2022

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

@brettchabot
Copy link
Collaborator

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.

@lsuski
Copy link
Author

lsuski commented Dec 5, 2022

@brettchabot yes, you're right

@eduardbosch-jt
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests