vendredi 29 mai 2020

Android Espresso - How to check if a view is ready to be clicked

I have an app with a splashscreen, which stays for about 2 seconds.

After that, it switches to another activity A. In A, I set a value in a SeekBar and after that, click a Button to confirm.

When I simply start a recorded Espresso test doing this, it tries to play while on the splashscreen. So when it tried to set the SeekBar value or click the Button, I get a NoMatchingViewException. So my first attempt at fixing this was to simply add a sleep(5000). This worked.

However, I dont want to put a manual sleep in after every Activity switch.

  1. Because it seems like unnecessary code
  2. Because it would mean unnecessary waiting time for running the test
  3. The timing might be arbitrary and could be different for different devices

So I tried to check whether or not Im in the right Activity/can see the right views. I did this using some SO links: Wait for Activity and Wait for View.

However, even that does not work 100%.

I have these two functions:

fun <T: AppCompatActivity> waitForActivity(activity: Class<T>, timeout: Int = 5000, waitTime: Int = 100) {
    val maxTries = timeout / waitTime
    var tries = 0

    for(i in 0..maxTries) {
        var currentActivity: Activity? = null
        getInstrumentation().runOnMainSync { run { currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(
            Stage.RESUMED).elementAtOrNull(0) } }
        if(activity.isInstance(currentActivity)) {
            break
        } else {
            tries++
            sleep(waitTime.toLong())
        }
    }
}
fun waitForView(
    @IntegerRes id: Int,
    waitMillis: Int = 5000,
    waitMillisPerTry: Long = 100
): ViewInteraction {

    // Derive the max tries
    val viewMatcher = allOf(
        withId(id),
        isDisplayed()
    )
    val maxTries = waitMillis / waitMillisPerTry.toInt()
    var tries = 0
    for (i in 0..maxTries)
        try {
            tries++
            val element = onView(viewMatcher)
            element.check { view, noViewFoundException ->
                if(view == null) {
                    throw noViewFoundException ?: Exception("TEST")
                }
                if(view.hasWindowFocus()) {
                    throw noViewFoundException ?: Exception("TEST2")
                }
            }
            return element
        } catch (e: Exception) {
            if (tries == maxTries) {
                throw e
            }
            sleep(waitMillisPerTry)
        }
    throw Exception("Error finding a view matching $viewMatcher")
}

Neither of those work 100%. Both of them seem to return within the timeout restrictions, and have "found" the activity/view. However, the expected view, e.g. a Button is not yet ready to perform, for example, element.perform(click()). It does not lead to a NoMatchingViewException, but it does not perform the click I did either. For the SeekBar, I use this:

private fun setSeekValue(seekBar: ViewInteraction, age: Int) {
        val fullPercentage = .9f
        val step = 1/99f

        seekBar.perform(
            GeneralClickAction(
                Tap.SINGLE,
                CoordinatesProvider { view ->
                    val pos = IntArray(2)
                    view?.getLocationOnScreen(pos)
                    FloatArray(2).apply {
                        this[0] = pos[0] + view!!.width * (.05f + fullPercentage*step*age)
                        this[1] = pos[1] + view.height * .5f
                    }
                },
                PrecisionDescriber {
                    FloatArray(2).apply {
                        this[0] = .1f
                        this[1] = 1f
                    }
                },
                InputDevice.SOURCE_MOUSE,
                MotionEvent.ACTION_BUTTON_PRESS
            )
        )
    }

However, when I use these functions and just put a very short sleep, e.g. sleep(100) after it, it works. This again however, would go against the three reasons listed above, which im trying to avoid here.

As you can see in the function waitForView, I tried to check if the View is "usable", using hasWindowFocus(). But this still does not perform the click, except for when I again put a sleep(80) or something after it. So it waits for the splashscreen to switch to A, finds the view it's looking for and then cant perform the click, except for when I wait a little bit.

I have also tried these functions of View:

  • isEnabled
  • isShown
  • visibility
  • getDrawingRect
  • isFocusable
  • isFocused
  • isLayoutDirectionResolved

Neither of them worked as I expected. With all of them, after the needed value was returned on the element.check part of waitForView, they would still not be accessible without putting a short sleep after.

Is there a way to reliably check if I can perform a click on a view/safely can perform ViewInteraction.perform()

Either by checking, if an activity is fully loaded to a point where its views are usable. Or by directly checking if a view is usable.

Aucun commentaire:

Enregistrer un commentaire