Android Espresso testing SwipeRefreshLayout OnRefresh not been triggered on swipeDown - android

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()));

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.

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())
}
}
}
}

Related

Prevent LaunchedEffect from re-running on configuration change

I want to run the code only once when the composable is loaded. So I am using LaunchedEffect with key as true to achieve this.
LaunchedEffect(true) {
// do API call
}
This code is working fine but whenever there is any configuration change like screen rotation this code is executed again. How can I prevent it from running again in case of configuration change?
The simplest solution is to store information about whether you made an API call with rememberSaveable: it will live when the configuration changes.
var initialApiCalled by rememberSaveable { mutableStateOf(false) }
if (!initialApiCalled) {
LaunchedEffect(Unit) {
// do API call
initialApiCalled = false
}
}
The disadvantage of this solution is that if the configuration changes before the API call is completed, the LaunchedEffect coroutine will be cancelled, as will your API call.
The cleanest solution is to use a view model, and execute the API call inside init:
class ScreenViewModel: ViewModel() {
init {
viewModelScope.launch {
// do API call
}
}
}
#Composable
fun Screen(viewModel: ScreenViewModel = viewModel()) {
}
Passing view model like this, as a parameter, is recommended by official documentation. In the prod code you don't need to pass any parameter to this view, just call it like Screen(): the view model will be created by default viewModel() parameter. It is moved to the parameter for test/preview capability as shown in this answer.
I assume the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the loadState is called only one time, and also guarantee the service itself is not called unless someone is listening to it. Then you listen to the state from the viewmodel, and no need to call anything api call from launched effect, also your code will be reacting to specic state.
Here is a code example
class MyViewModel : ViewModel() {
private val uiScreenState: : MutableStateFlow<WhatEverState> =
MutableStateFlow(WhatEverIntialState).also {
loadState()
}
fun loadState(): StateFlow<WhatEverState>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
When using this code, you do not have to call loadstate at all in the activity, you just listen to the observer.
You may check the below code for the listening
class MyFragment : Fragment {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
StartingComposeTheme {
Box(modifier = Modifier.fillMaxSize()) {
val state by viewModel.uiScreenState.collectAsState()
when (state) {
//do something
}
}
}
}
}
}
}}
#Islam Mansour answer work good for dedicated viewModel to UI but my case is shared ViewModel by many UIs fragments
In my case above answers does not solve my problem for calling API for just only first time call when user navigate to the concerned UI section.
Because I have multiple composable UIs in NavHost as Fragment
And my ViewModel through all fragments
so, the API should only call when user navigate to the desired fragment
so, the below lazy property initialiser solve my problem;
val myDataList by lazy {
Log.d("test","call only once when called from UI used inside)")
loadDatatoThisList()
mutableStateListOf<MyModel>()
}
mutableStateListOf<LIST_TYPE> automatically recompose UI when data added to this
variable appeded by by lazy intialized only once when explicilty called

How to wait and continue execution in Kotlin

basically, I am trying to continue execution right after the ad is finished.
Currently, I am using delay which is not really convenient. Some user won't wait or simply dismiss the activity while the delays happen.
My current approach:
override fun onRequestStarted() {
showAd()
}
private fun Context.showAd(): Boolean {
mInterstitialAd?.fullScreenContentCallback = object : FullScreenContentCallback() {
/** Some other function **/
override fun onAdShowedFullScreenContent() {
isAdWatched = true
}
}
override fun onRequestIntent() {
GlobalScope.launch {
delay(10000L)
if (isAdWatched) {
isAdWatched = false
super.onRequestIntent(intent)
}
}
}
You can convert callback-based APIs to suspend functions.
To create a suspend function that returns when the callback fires. For this example, I'm assuming you only want to await the return from the ad, whether or not it was watched. I haven't tested this, but the documentation implies that this particular function onAdDismissedFullScreenContent is the only one you need to respond to to know when the ad is finished (or never loaded).
/** Show the ad and suspend until it is dismissed. */
suspend fun InterstitialAd.showAndAwait(activity: Activity) = suspendCoroutine<Unit> { cont ->
fullScreenContentCallback = object : FullScreenContentCallback() {
override onAdDismissedFullScreenContent() {
cont.resume(Unit)
}
}
showAd(activity)
}
You will have to change your design so the functionality isn't split between these two functions that you overrode. I don't know how you're calling these two functions, so I can't exactly suggest what you need to change. But ultimately, to do what you described, you would have a single function to call that launches a coroutine, and in the coroutine calls the above suspend function to show the ad and then does whatever you want to do next. Something like this:
fun foo() {
lifecycleScope.launch {
mInterstitialAd?.showAndAwait()
doSomethingAfterReturnedFromAd()
}
}
Never use GlobalScope. In the latest version of Kotlin Coroutines, it shows a compiler warning when you use it, although it's not quite deprecated because there are a few very specific cases where it might have useful applications. You should use lifecycleScope for this.

Espresso, how to scroll to bottom and IDLE state of recycler view

I am learning UI testing with Espresso. I want to test scrolling of recycler view to bottom and only then load next page from view model and pass it recycler view.
I have following onScrollListener in my fragment:
private fun setupOnScrollListener() {
recyclerViewApi.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
val isRecyclerViewBottom = !recyclerView.canScrollVertically(1) &&
newState == RecyclerView.SCROLL_STATE_IDLE
if (isRecyclerViewBottom) {
downloadNextPage()
}
}
})
}
private fun downloadNextPage() {
showProgressBar(true)
viewModel.getNextMovies()
}
When I test it manually with Log.d() it works great.
My question is: How to use Espresso (or maybe different API, if you know better than Espresso) to scroll recycler view to this state:
isRecyclerViewBottom = !recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE,
so my downloadNextPage() will be invoked and test function will pull more data.
My test function:
#Test
fun scrollToBottom_isNextPageLoaded(){
every { repository.getApiMovies(any(), any()) } returns
Flowable.just(Resource.success(moviesList1_5)) andThen
Flowable.just(Resource.success(moviesList1_10))
val scenario = launchFragmentInContainer<ApiFragment>(factory = fragmentsFactory)
//first 5 items are in view, so I go to the last item (index 4)
recyclerView.perform(scrollToPosition<ViewHolder>(4))
recyclerView.perform(swipeDown())
//Below doesn't make any difference
Thread.sleep(1000L)
verify(exactly = 2) { repo.getApiMovies(any(), any()) }
}
I use Robolectric, Mockk, Espresso. I have mocked here repository class, which is passed to constructor of ViewModelFactory, which is passed to constructor of the ApiFragment.
Message from JUnit:
java.lang.AssertionError: Verification failed: call 1 of 1: ApiRepository(repo#4).getApiMovies(any(), any())).
One matching call found, but needs at least 2 and at most 2 calls
Call: ApiRepository(repo#4).getApiMovies(Top Rated, 1)
It is not my first test function. Everything else works great. I just don't know how to make Espresso to go to bottom of recycler view and 'pull up' bottom edge of it to call downloadNextPage()
Ok. I have just found a sollution. I changed recyclerView.perform(swipeDown()) to recyclerView.perform(swipeUp()).

RxJava - Use flowables async way

Re-edit : I will use the result of this method to initialize the visibility of some buttons in my view
The accepted answer seems good in theory but the return value of first method is Flowable> instead of Flowable. Hence I cannot pass it as a parameter to subscription since it requires Flowable but it is Flowable>
Question Before Edit
I am using RxJava to observe a method I am required to call from SDK. Using this method I am trying to make an assertion about the existence of something but I do not know how long the call will take so it is hard for me to say terminate the subscription after x seconds.
override fun doesExist(): Boolean {
var doesExist = false
var subscription : Subscription
val flowable = Flowable.just(SDK.searchContact("contact"))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : FlowableSubscriber<Flowable<List<Contact>>> {
override fun onSubscribe(s: Subscription) {
subscription = s
}
override fun onNext(t: Flowable<List<Contact>>?) {
doesExist = true
}
override fun onComplete() {
Log.d("tagtagcomplete", "tagtagcomplete")
}
override fun onError(e: Throwable?) {
Log.d("tagtagerror", "tagtagerror")
}
return doesExist
}
So what I want to do is return true if I won't get any result from searchContract method and return false if I get a result. While using observables, I can create a subscription object and call it's methods but I could not figure out how to do it right way with flowables.
My confusion is the following: Method to sdk returns a Flowable<List<Contact>>> but in my opinion I need to check if a contact exists only once and stop
Right now my method does not go inside onError, onNext or onComplete. It just returns doesExist
I reedit my answer,following return type is you need.
fun doesExist(): Flowable<Single<Boolean>> {
return Flowable.just(Single.just(SDK.searchContact("contact")).map{ it.isEmpty()})
}

BottomSheetBehaviour setstate without animation

I have tried the new BottomSheetBehaviour with design library 23.0.2 but i think it too limited. When I change state with setState() method, the bottomsheet use ad animation to move to the new state.
How can I change state immediately, without animation? I don't see a public method to do that.
Unfortunately it looks like you can't. Invocation of BottomSheetBehavior's setState ends with synchronous or asynchronous call of startSettlingAnimation(child, state). And there is no way to override these methods behavior cause setState is final and startSettlingAnimation has package visible modifier. Check the sources for more information.
I have problems with the same, but in a bit different way - my UI state changes setHideable to false before that settling animation invokes, so I'm getting IllegalStateException there. I will consider usage of BottomSheetCallback to manage this properly.
If you want to remove the show/close animation you can use dialog.window?.setWindowAnimations(-1). For instance:
class MyDialog(): BottomSheetDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
dialog.window?.setDimAmount(0f) // for removing the dimm
dialog.window?.setWindowAnimations(-1) // for removing the animation
return dialog
}
}
If you really need it, then you can resort to reflection:
fun BottomSheetBehavior.getViewDragHelper(): ViewDragHelper? = BottomSheetBehavior::class.java
.getDeclaredField("viewDragHelper")
.apply { isAccessible = true }
.let { field -> field.get(this) as? ViewDragHelper? }
fun ViewDragHelper.getScroller(): OverScroller? = ViewDragHelper::class.java
.getDeclaredField("mScroller")
.apply { isAccessible = true }
.let { field -> field.get(this) as? OverScroller? }
Then you can use these extension methods when the state changes:
bottomSheetBehavior.setBottomSheetCallback(object : BottomSheetCallback() {
override fun onSlide(view: View, offset: Float) {}
override fun onStateChanged(view: View, state: Int) {
if (state == STATE_SETTLING) {
try {
bottomSheetBehavior.getViewDragHelper()?.getScroller()?.abortAnimation()
} catch(e: Throwable) {}
}
}
})
I will add that the code is not perfect, getting fields every time the state changes is not efficient, and this is done for the sake of simplicity.

Categories

Resources