1

This is my viewmodel class :

class MainViewModel(
    private val schedulerProvider: BaseSchedulerProvider,
    private val api : StorytelService
) : BaseViewModel() {

    private val _posts = MutableLiveData<List<Post>>()
    val posts: LiveData<List<Post>>
        get() = _posts

    private val _status = MutableLiveData<Status>()
    val status: LiveData<Status>
        get() = _status

    init {
        showPhotos()
    }

    fun showPhotos() {
        EspressoIdlingResource.increment() // App is busy until further notice
        _status.postValue(Status.LOADING)
        compositeDisposable.add(api.getPhotos()
            .subscribeOn(schedulerProvider.io())
            .observeOn(schedulerProvider.ui())
            .doFinally {
                if (!EspressoIdlingResource.countingIdlingResource.isIdleNow) {
                    EspressoIdlingResource.decrement() // Set app as idle.
                }
            }
            .subscribe({
                _status.postValue(Status.SUCCESS)
                showPosts(it)
            }) {
                _status.postValue(Status.ERROR)
                Timber.e(it)
            })
    }

    private fun showPosts(networkPhotos: List<NetworkPhoto>) {
        EspressoIdlingResource.increment() // App is busy until further notice
        _status.postValue(Status.LOADING)
        compositeDisposable.add(api.getPosts()
            .subscribeOn(schedulerProvider.io())
            .observeOn(schedulerProvider.ui())
            .doFinally {
                if (!EspressoIdlingResource.countingIdlingResource.isIdleNow) {
                    EspressoIdlingResource.decrement() // Set app as idle.
                }
            }
            .subscribe({ networkPosts ->
                _status.postValue(Status.SUCCESS)
                _posts.postValue(
                    PostAndImages(networkPosts, networkPhotos).asDomaineModel()
                )
            }) {
                _status.postValue(Status.ERROR)
                Timber.e(it)
            })
    }

This is my recyclerView in layout :

<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_behavior="@string/appbar_scrolling_view_behavior"
            app:showData="@{vm.status}"
            tools:listitem="@layout/post_item" />

And here is binding adapter :

@BindingAdapter("showData")
fun View.showData(status: Status) {
    visibility = if (status == Status.SUCCESS) View.VISIBLE else View.GONE
}

As you notice I am using EspressoIdlingResource, but when I run following espresso test, it fails :

    @Test
    fun shouldBeAbleToLoadList() {
        onView(withId(R.id.recycler_view)).check(matches(isDisplayed()))
    } 

If I add Thread.sleep(5000) in the beginning of the test, it works. How to resolve it?

Ali
  • 9,800
  • 19
  • 72
  • 152
  • Is `EspressoIdlingResource` a custom class? Did you forget to register the idling resource before the test runs? – Aaron Oct 21 '19 at 21:10
  • No I did not forget, you can look at it : https://bitbucket.org/ali-rezaei/storytel/src/master/ – Ali Oct 22 '19 at 08:51
  • It looks like you have post animation, have you tried turning off the animation on device? And also if you're using databinding, this answer maybe helpful https://stackoverflow.com/questions/40703567/how-do-i-make-espresso-wait-until-data-binding-has-updated-the-view-with-the-dat – Aaron Oct 22 '19 at 22:26

2 Answers2

2

It should be possible with Idling Resource however they are a little bit tedious.

I've just updated an old viewMatcher code:

/**
 * Perform action of waiting for a specific view id to be displayed.
 * @param viewId The id of the view to wait for.
 * @param millis The timeout of until when to wait for.
 */
public static ViewAction waitDisplayed(final int viewId, final long millis) {
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return isRoot();
        }

        @Override
        public String getDescription() {
            return "wait for a specific view with id <" + viewId + "> has been displayed during " + millis + " millis.";
        }

        @Override
        public void perform(final UiController uiController, final View view) {
            uiController.loopMainThreadUntilIdle();
            final long startTime = System.currentTimeMillis();
            final long endTime = startTime + millis;
            final Matcher<View> matchId = withId(viewId);
            final Matcher<View> matchDisplayed = isDisplayed();

            do {
                for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
                    if (matchId.matches(child) && matchDisplayed.matches(child)) {
                        return;
                    }
                }

                uiController.loopMainThreadForAtLeast(50);
            }
            while (System.currentTimeMillis() < endTime);

            // timeout happens
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(new TimeoutException())
                    .build();
        }
    };
}

then you should only do:

@Test
    fun shouldBeAbleToLoadList() {
        onView(isRoot()).perform(waitDisplayed(R.id.recycler_view, 5000));
    } 

the 5000 is a timeout of 5 secs (5000 millis), you can change it if you want to.

After waitDisplayed is executed it could happen that the element is shown or the timeout has been reached. In the last case an Exception will be thrown.

jeprubio
  • 17,312
  • 5
  • 45
  • 56
  • Thanks. This is a working solution. Can I ask you where did you find it? – Ali Oct 21 '19 at 15:31
  • You can mark the answer as correct or mark the answer as useful if it works for you. I've just adapted the `waitId` matcher of this other question: https://stackoverflow.com/questions/49796132/android-espresso-wait-for-text-to-appear – jeprubio Oct 21 '19 at 15:46
0

You will need to create an Idling Resource for the binding. You can check Android Architecture Components sample which have a similar implementation. Here's what you will need to look for:

  1. Firstly, you will need to add an Idling Resource class which checks if there are any bindings pending (you can find an implementation here)
  2. Now you can create a rule which will automatically register/unregister Idling Resource for you (you can find an implementation here).
  3. And now you can add this rule to your test and check if it works (sample test implementation you can find here).
Maksym S.
  • 141
  • 4
  • I have followed you instruction, but it still fails. Could you please look at : https://bitbucket.org/ali-rezaei/storytel/src/Espresso/ and tell me why? – Ali Oct 21 '19 at 12:55