Mark
Mark

Reputation: 2542

Wait for view pager animations with espresso?

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?

Upvotes: 18

Views: 7296

Answers (7)

Adil Hussain
Adil Hussain

Reputation: 32103

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.

Upvotes: 4

Anders Ullnæss
Anders Ullnæss

Reputation: 946

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

Upvotes: 3

VadzimV
VadzimV

Reputation: 1261

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

Upvotes: 0

user2880229
user2880229

Reputation: 151

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;
        }
    }
}

Upvotes: 0

Rekha
Rekha

Reputation: 901

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

Upvotes: 0

vaughandroid
vaughandroid

Reputation: 4374

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

Upvotes: 29

Simas
Simas

Reputation: 44118

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

or simply:

SystemClock.sleep(500);

Upvotes: -11

Related Questions