Đorđe Hrnjez
Đorđe Hrnjez

Reputation: 61

Android Espresso testing SwipeRefreshLayout OnRefresh not been triggered on swipeDown

I'm trying to write simple test for pull to refresh as a part of integration testing. I'm using the newest androidX testing components and Robolectric. I'm testing isolated fragment in which one I'm injecting mocked presenter.

XML layout part

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerTasks"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

Fragment part

binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                presenter.onRefresh();
            }
        });

Test:

onView(withId(R.id.refreshLayout)).perform(swipeDown());
verify(presenter).onRefresh();

but test doesn't pass, message:

Wanted but not invoked: presenter.onRefresh();

The app works perfectly fine and pull to refresh calls presenter.onRefresh(). I did also debugging of the test and setOnRefreshListener been called and it's not a null. If I do testing with custom matcher to check the status of SwipeRefreshLayout test passes.

onView(withId(R.id.refreshLayout)).check(matches(isRefreshing()));

Upvotes: 5

Views: 2005

Answers (2)

Faruk
Faruk

Reputation: 5821

I'm finally able to solve this using a hacky way :

fun swipeToRefresh(): ViewAction {
    return object : ViewAction {
        override fun getConstraints(): Matcher<View>? {
            return object : BaseMatcher<View>() {
                override fun matches(item: Any): Boolean {
                    return isA(SwipeRefreshLayout::class.java).matches(item)
                }
                override fun describeMismatch(item: Any, mismatchDescription: Description) {
                    mismatchDescription.appendText(
                        "Expected SwipeRefreshLayout or its Descendant, but got other View"
                    )
                }
                override fun describeTo(description: Description) {
                    description.appendText(
                        "Action SwipeToRefresh to view SwipeRefreshLayout or its descendant"
                    )
                }
            }
        }

        override fun getDescription(): String {
            return "Perform swipeToRefresh on the SwipeRefreshLayout"
        }

        override fun perform(uiController: UiController, view: View) {
            val swipeRefreshLayout = view as SwipeRefreshLayout
            swipeRefreshLayout.run {
                isRefreshing = true
                // set mNotify to true
                val notify = SwipeRefreshLayout::class.memberProperties.find {
                    it.name == "mNotify"
                }
                notify?.isAccessible = true
                if (notify is KMutableProperty<*>) {
                    notify.setter.call(this, true)
                }
                // mockk mRefreshListener onAnimationEnd
                val refreshListener = SwipeRefreshLayout::class.memberProperties.find {
                    it.name == "mRefreshListener"
                }
                refreshListener?.isAccessible = true
                val animatorListener = refreshListener?.get(this) as Animation.AnimationListener
                animatorListener.onAnimationEnd(mockk())
            }
        }
    }
}

Upvotes: 2

Kelsos
Kelsos

Reputation: 146

I did some minor investigation over last weekend since I was facing the same issue and it was bothering me. I also did some comparing with what happens on a device to spot the differences.

Internally androidx.swiperefreshlayout.widget.SwipeRefreshLayout has an mRefreshListener that will run when onAnimationEnd is called. The AnimationEnd will trigger then OnRefreshListener.onRefresh method.

That animation listener (mRefreshListener) is passed to the mCircleView (CircleImageView) and the circle animation start is called.

On a device when the view draw method is called it will call the applyLegacyAnimation method that will, in turn, call the AnimationStart method. At the AnimationEnd, the onRefresh method will be called.

On Robolectric the draw method of the View is never called since the items are not actually drawn. This means that the animation will never run and thus neither will the onRefresh method.

My conclusion is that with the current version of Robolectric is not possible to verify that the onRefresh called due to implementation limitations. It seems though that it is planned to have a realistic rendering in the future.

Upvotes: 5

Related Questions