17

Trying to do some tests with a ViewPager.

I want to swipe between tabs, and I don't want to continue until the swipe is complete. But there doesn't appear to be a way to turn off the animation for the view pager (all animations under the developer options are disabled).

So this always results in a test failure, because the view pager hasn't completed it's animation, and so the view is not completely displayed yet:

// swipe left
onView(withId(R.id.viewpager)).check(matches(isDisplayed())).perform(swipeLeft());

// check to ensure that the next tab is completely visible.
onView(withId(R.id.next_tab)).check(matches(isCompletelyDisplayed()));

Is there an elegant or maybe even recommended way to do this, or am I stuck putting some kind of timed wait in there?

Lorenzo Polidori
  • 10,332
  • 10
  • 51
  • 60
Mark
  • 2,362
  • 18
  • 34

7 Answers7

29

The IdlingResource @Simas suggests is actually pretty simple to implement:

public class ViewPagerIdlingResource implements IdlingResource {

    private final String mName;

    private boolean mIdle = true; // Default to idle since we can't query the scroll state.

    private ResourceCallback mResourceCallback;

    public ViewPagerIdlingResource(ViewPager viewPager, String name) {
        viewPager.addOnPageChangeListener(new ViewPagerListener());
        mName = name;
    }

    @Override
    public String getName() {
        return mName;
    }

    @Override
    public boolean isIdleNow() {
        return mIdle;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        mResourceCallback = resourceCallback;
    }

    private class ViewPagerListener extends ViewPager.SimpleOnPageChangeListener {

        @Override
        public void onPageScrollStateChanged(int state) {
            mIdle = (state == ViewPager.SCROLL_STATE_IDLE
                    // Treat dragging as idle, or Espresso will block itself when swiping.
                    || state == ViewPager.SCROLL_STATE_DRAGGING);
            if (mIdle && mResourceCallback != null) {
                mResourceCallback.onTransitionToIdle();
            }
        }
    }
}
Lorenzo Polidori
  • 10,332
  • 10
  • 51
  • 60
vaughandroid
  • 4,315
  • 1
  • 27
  • 33
  • 2
    Am I right in thinking I only have to register the ViewPagerIdlingResource once per test. So I register the VPIR at the beginning of the test, then onView().perform(swipeLeft), then again onView().perform(swipeLeft), then I check my view. I don't have to unregister and register my VPIR between the two swipes right? – flobacca Jan 06 '17 at 22:20
  • This did not work for me, I need to treat ViewPager.SCROLL_STATE_DRAGGING state as not idle but if I remove it espresso blocks itself as the comment in the code says. Any idea why this happens? – user2880229 Apr 16 '18 at 22:27
  • Why exactly do you need to treat the dragging state as not idle? – vaughandroid Apr 17 '18 at 05:46
  • When I run the tests, it seems that Espresso is not waiting for the swiping to be completed, thus, it tries to match views before they are completely visible. With this approach the tests were passing sometimes and sometimes not. I provided a slightly different implementation, it seems to work so far. – user2880229 Apr 17 '18 at 18:37
3

Since I've done this at least twice now, here is the accepted answer in Kotlin and with androidx ViewPager2:

class ViewPager2IdlingResource(viewPager: ViewPager2, name: String) : IdlingResource {

    private val name: String
    private var isIdle = true // Default to idle since we can't query the scroll state.
    private var resourceCallback: IdlingResource.ResourceCallback? = null

    init {
        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrollStateChanged(state: Int) {
                isIdle = (state == ViewPager.SCROLL_STATE_IDLE // Treat dragging as idle, or Espresso will block itself when swiping.
                    || state == ViewPager.SCROLL_STATE_DRAGGING)
                if (isIdle && resourceCallback != null) {
                    resourceCallback!!.onTransitionToIdle()
                }
            }
        })
        this.name = name
    }

    override fun getName(): String {
        return name
    }

    override fun isIdleNow(): Boolean {
        return isIdle
    }

    override fun registerIdleTransitionCallback(resourceCallback: IdlingResource.ResourceCallback) {
        this.resourceCallback = resourceCallback
    }
}

And here is how you use it from a UI test using ActivityScenarioRule:

@get:Rule
val testRule = ActivityScenarioRule(OnboardingActivity::class.java)

private lateinit var viewPager2IdlingResource: ViewPager2IdlingResource

....

@Before
fun setUp() {
    testRule.scenario.onActivity {
        viewPager2IdlingResource =
            ViewPager2IdlingResource(it.findViewById(R.id.onboarding_view_pager), "viewPagerIdlingResource")
        IdlingRegistry.getInstance().register(viewPager2IdlingResource)
    }
}

@After
fun tearDown() {
    IdlingRegistry.getInstance().unregister(viewPager2IdlingResource)
}
Anders Ullnæss
  • 818
  • 9
  • 8
3

The androidx.test.espresso:espresso-core library offers a ViewPagerActions class which contains a number of methods for scrolling between the pages of a ViewPager. It takes care of waiting until the scroll is complete so you don't need to add any explicit waits or sleeps in your test methods.

If you need to perform similar scrolling on a ViewPager2 instance, you can take the source code of the ViewPagerActions class and make some minor tweaks to it to get it to work for ViewPager2. Here is an example which you are welcome to take and use.

Adil Hussain
  • 30,049
  • 21
  • 112
  • 147
  • 1
    Thanks for this migration ! `ViewPager2Actions` is working really well and make my test working faster ! – LeMimit Jan 05 '23 at 17:18
0

Try this,

    onView(withId(R.id.pager)).perform(pagerSwipeRight()).perform(pagerSwipeLeft());

    private GeneralSwipeAction pagerSwipeRight(){
        return new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT,
                GeneralLocation.CENTER_RIGHT, Press.FINGER);
    }

    private GeneralSwipeAction pagerSwipeLeft(){
        return new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_RIGHT,
                GeneralLocation.CENTER_LEFT, Press.FINGER);
    }
Rekha
  • 901
  • 1
  • 11
  • 20
  • Just by introducing a custom swipe and replacing `Swipe.FAST` with `Swipe.SLOW` might not fix the core issue & I have tried this already! The better way would be to create a Custom `IdlingResource` as suggested in the [answer above](http://stackoverflow.com/a/32763454/1016544). – Wahib Ul Haq Jan 05 '17 at 17:11
0

I was having issues with @vaughandroid approach, so I did some changes to his approach. This approach will set idle to false as soon as it detects a scrolling is happening and "force" the ViewPager to finish scrolling by using setCurrentItem().

public class ViewPagerIdlingResource implements IdlingResource {
    private volatile boolean mIdle = true; // Default to idle since we can't query the scroll state.

    private ResourceCallback mResourceCallback;

    private ViewPager mViewPager;

    public static ViewPagerIdlingResource waitViewPagerSwipe(ViewPager viewPager) {
        return new ViewPagerIdlingResource(viewPager);
    }

    private ViewPagerIdlingResource(ViewPager viewPager) {
        mViewPager = viewPager;
        mViewPager.addOnPageChangeListener(new ViewPagerListener());
    }

    @Override
    public String getName() {
        return ViewPagerIdlingResource.class.getSimpleName();
    }

    @Override
    public boolean isIdleNow() {
        return mIdle;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
        mResourceCallback = resourceCallback;
    }

    private class ViewPagerListener extends ViewPager.SimpleOnPageChangeListener {
        float mPositionOffset = 0.0f;

        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            if (isSwipingToRight(positionOffset)) {
                mIdle = false;
                mViewPager.setCurrentItem(position + 1);
            } else if (isSwipingToLeft(positionOffset)) {
                mIdle = false;
                mViewPager.setCurrentItem(position - 1);
            }

            mPositionOffset = positionOffset;

            if (positionOffset == 0 && !mIdle && mResourceCallback != null) {
                mResourceCallback.onTransitionToIdle();
                mIdle = true;
                mPositionOffset = 0.0f;
            }
        }

        private boolean isSwipingToRight(float positionOffset) {
            return mPositionOffset != 0.0f && positionOffset > mPositionOffset && mIdle;
        }

        private boolean isSwipingToLeft(float positionOffset) {
            return mPositionOffset != 0.0f && positionOffset < mPositionOffset && mIdle;
        }
    }
}
user2880229
  • 151
  • 8
0

My goal was to make a screenshot of the screen with ViewPager2 using Facebook screenshot test library. The easiest approach for me was to check almost every frame whether animation completed, if yes then it's time to make a screenshot:

fun waitForViewPagerAnimation(parentView: View) {
    if (parentView is ViewGroup) {
        parentView.childrenViews<ViewPager2>().forEach {
            while (it.scrollState != ViewPager2.SCROLL_STATE_IDLE) {
                Thread.sleep(16)
            }
        }
    }
}

childrenViews function can be found here

VadzimV
  • 1,111
  • 12
  • 13
-11

You can either do a lot of work and use an IdlingResource to implement an OnPageChangeListener

or simply:

SystemClock.sleep(500);
Simas
  • 43,548
  • 10
  • 88
  • 116
  • The sleep is what I'd really like to avoid. – Mark Jun 27 '15 at 21:24
  • 3
    Sleeping animation times is a very unmaintainable practice. Also it clutters up test code, as it can quickly consist of 50% sleep statements. You should really go for the idling resource solution, which pulls out idle waiting from your test code logic. – david.schreiber Oct 07 '15 at 07:55
  • This can also result in inconsistent, flaky tests because different systems or iterations of the test need more/less time to wait. It can also slowly start to add a significant amount of time to your test suite. – Andrea Thacker May 15 '17 at 16:11