KakaoCup / Kakao

Nice and simple DSL for Espresso in Kotlin
https://kakaocup.github.io/Kakao/
Apache License 2.0
325 stars 28 forks source link

KRecyclerView: RecyclerActions::scrollToEnd() throws NullPointerException #60

Open cee-dee opened 2 years ago

cee-dee commented 2 years ago

Steps to reproduce:

  1. Use long-loading RecyclerView items in a standard RecyclerView
  2. call scrollToEnd() in a UI test

Observed Results:

The code

                    val lastView = view.findViewHolderForLayoutPosition(position)!!.itemView
                    view.scrollBy(0, lastView.height)

throws a NullPointerException because lastView is null.

Expected Results:

I expected the RecyclerView just to scroll down to have the last item fully visible.

Relevant Code:

    override fun scrollToEnd() {
        view.perform(object : ViewAction {
            override fun getDescription() = "Scroll RecyclerView to the bottom"

            override fun getConstraints() = ViewMatchers.isAssignableFrom(RecyclerView::class.java)

            override fun perform(controller: UiController, view: View) {
                if (view is RecyclerView) {
                    val position = view.adapter!!.itemCount - 1
                    view.scrollToPosition(position)
                    controller.loopMainThreadUntilIdle()
                    val lastView = view.findViewHolderForLayoutPosition(position)!!.itemView
                    view.scrollBy(0, lastView.height)
                    controller.loopMainThreadUntilIdle()
                }
            }
        })
    }

Workaround:

I've created an extension function to still be able to do what I'd like to do:

fun KRecyclerView.scrollToEndRepeatedly(repetitions: Int) {

    view.perform(
        object : ViewAction {
            override fun getDescription() =
                "Scroll RecyclerView to the bottom"

            override fun getConstraints() =
                ViewMatchers.isAssignableFrom(
                    RecyclerView::class.java
                )

            override fun perform(controller: UiController, view: View) {
                if (view is RecyclerView) {
                    var lastViewFound = false
                    var tryCount = 0
                    do {
                        tryCount++
                        val position = view.adapter!!.itemCount - 1
                        view.scrollToPosition(position)
                        controller.loopMainThreadUntilIdle()
                        val lastView =
                            view.findViewHolderForLayoutPosition(
                                position
                            )
                        lastView?.let {
                            view.scrollBy(0, lastView.itemView.height)
                            lastViewFound = false
                        }
                        controller.loopMainThreadUntilIdle()
                    } while ((!lastViewFound) && (tryCount < repetitions))
                }
            }

        }
    )
}

While this does what it's supposed to do, I think, there must be a better solution using interceptors which fit's more naturally into Kakaos concepts, i.e. making the repetions parameter superfluous.

AlexeyRybakov commented 1 year ago

Same problem

Vacxe commented 1 year ago

@cee-dee @AlexeyRybakov what do you mean under "long-loading items"? If it some view what related on async calls - you may use IdleResources for it. Otherwise it those views is "long-loaded" because of device performance it should't be a problem because it will block UI thread.

@Unlimity please leave you comments

Unlimity commented 1 year ago

If your RecyclerView's adapter and layout manager cannot layout all children in a single layout pass - there is not much Espresso and Kakao can do for you. You either need to optimize your RecyclerView to be able to layout all items in adapter as it is expected by the system, or use your own extension or try/catch with retry blocks in the test itself. This is a very specific corner case and is not system expected behavior, so I don't see a lot of value into supporting it as part of the library.