1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.support.v4.view;
17
18import static android.support.test.espresso.Espresso.onView;
19import static android.support.test.espresso.action.ViewActions.pressKey;
20import static android.support.test.espresso.action.ViewActions.swipeLeft;
21import static android.support.test.espresso.action.ViewActions.swipeRight;
22import static android.support.test.espresso.assertion.PositionAssertions.isBelow;
23import static android.support.test.espresso.assertion.PositionAssertions.isBottomAlignedWith;
24import static android.support.test.espresso.assertion.PositionAssertions.isLeftAlignedWith;
25import static android.support.test.espresso.assertion.PositionAssertions.isRightAlignedWith;
26import static android.support.test.espresso.assertion.PositionAssertions.isTopAlignedWith;
27import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
28import static android.support.test.espresso.assertion.ViewAssertions.matches;
29import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
30import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
31import static android.support.test.espresso.matcher.ViewMatchers.withId;
32import static android.support.test.espresso.matcher.ViewMatchers.withText;
33import static android.support.v4.testutils.TestUtilsAssertions.hasDisplayedChildren;
34import static android.support.v4.testutils.TestUtilsMatchers.backgroundColor;
35import static android.support.v4.testutils.TestUtilsMatchers.centerAlignedInParent;
36import static android.support.v4.testutils.TestUtilsMatchers.endAlignedToParent;
37import static android.support.v4.testutils.TestUtilsMatchers.isOfClass;
38import static android.support.v4.testutils.TestUtilsMatchers.startAlignedToParent;
39import static android.support.v4.view.ViewPagerActions.arrowScroll;
40import static android.support.v4.view.ViewPagerActions.scrollLeft;
41import static android.support.v4.view.ViewPagerActions.scrollRight;
42import static android.support.v4.view.ViewPagerActions.scrollToFirst;
43import static android.support.v4.view.ViewPagerActions.scrollToLast;
44import static android.support.v4.view.ViewPagerActions.scrollToPage;
45import static android.support.v4.view.ViewPagerActions.setAdapter;
46import static android.support.v4.view.ViewPagerActions.wrap;
47
48import static org.hamcrest.MatcherAssert.assertThat;
49import static org.hamcrest.Matchers.allOf;
50import static org.hamcrest.Matchers.is;
51import static org.hamcrest.core.IsNot.not;
52import static org.junit.Assert.assertEquals;
53import static org.junit.Assert.assertFalse;
54import static org.junit.Assert.assertTrue;
55import static org.mockito.Mockito.anyInt;
56import static org.mockito.Mockito.atLeastOnce;
57import static org.mockito.Mockito.mock;
58import static org.mockito.Mockito.never;
59import static org.mockito.Mockito.times;
60import static org.mockito.Mockito.verify;
61
62import android.app.Activity;
63import android.graphics.Color;
64import android.support.coreui.test.R;
65import android.support.test.espresso.ViewAction;
66import android.support.test.espresso.action.EspressoKey;
67import android.support.test.filters.LargeTest;
68import android.support.test.filters.MediumTest;
69import android.support.v4.BaseInstrumentationTestCase;
70import android.support.v4.testutils.TestUtilsMatchers;
71import android.text.TextUtils;
72import android.util.Pair;
73import android.view.KeyEvent;
74import android.view.View;
75import android.view.ViewGroup;
76import android.widget.Button;
77import android.widget.LinearLayout;
78import android.widget.TextView;
79
80import org.junit.After;
81import org.junit.Assert;
82import org.junit.Before;
83import org.junit.Test;
84import org.mockito.ArgumentCaptor;
85
86import java.util.ArrayList;
87import java.util.List;
88
89/**
90 * Base class for testing <code>ViewPager</code>. Most of the testing logic should be in this
91 * class as it is independent on the specific pager title implementation (interactive or non
92 * interactive).
93 *
94 * Testing logic that does depend on the specific pager title implementation is pushed into the
95 * extending classes in <code>assertStripInteraction()</code> method.
96 */
97public abstract class BaseViewPagerTest<T extends Activity> extends BaseInstrumentationTestCase<T> {
98    private static final int DIRECTION_LEFT = -1;
99    private static final int DIRECTION_RIGHT = 1;
100    protected ViewPager mViewPager;
101
102    protected static class BasePagerAdapter<Q> extends PagerAdapter {
103        protected ArrayList<Pair<String, Q>> mEntries = new ArrayList<>();
104
105        public void add(String title, Q content) {
106            mEntries.add(new Pair<>(title, content));
107        }
108
109        @Override
110        public int getCount() {
111            return mEntries.size();
112        }
113
114        protected void configureInstantiatedItem(View view, int position) {
115            switch (position) {
116                case 0:
117                    view.setId(R.id.page_0);
118                    break;
119                case 1:
120                    view.setId(R.id.page_1);
121                    break;
122                case 2:
123                    view.setId(R.id.page_2);
124                    break;
125                case 3:
126                    view.setId(R.id.page_3);
127                    break;
128                case 4:
129                    view.setId(R.id.page_4);
130                    break;
131                case 5:
132                    view.setId(R.id.page_5);
133                    break;
134                case 6:
135                    view.setId(R.id.page_6);
136                    break;
137                case 7:
138                    view.setId(R.id.page_7);
139                    break;
140                case 8:
141                    view.setId(R.id.page_8);
142                    break;
143                case 9:
144                    view.setId(R.id.page_9);
145                    break;
146            }
147        }
148
149        @Override
150        public void destroyItem(ViewGroup container, int position, Object object) {
151            // The adapter is also responsible for removing the view.
152            container.removeView(((ViewHolder) object).view);
153        }
154
155        @Override
156        public int getItemPosition(Object object) {
157            return ((ViewHolder) object).position;
158        }
159
160        @Override
161        public boolean isViewFromObject(View view, Object object) {
162            return ((ViewHolder) object).view == view;
163        }
164
165        @Override
166        public CharSequence getPageTitle(int position) {
167            return mEntries.get(position).first;
168        }
169
170        protected static class ViewHolder {
171            final View view;
172            final int position;
173
174            public ViewHolder(View view, int position) {
175                this.view = view;
176                this.position = position;
177            }
178        }
179    }
180
181    protected static class ColorPagerAdapter extends BasePagerAdapter<Integer> {
182        @Override
183        public Object instantiateItem(ViewGroup container, int position) {
184            final View view = new View(container.getContext());
185            view.setBackgroundColor(mEntries.get(position).second);
186            configureInstantiatedItem(view, position);
187
188            // Unlike ListView adapters, the ViewPager adapter is responsible
189            // for adding the view to the container.
190            container.addView(view);
191
192            return new ViewHolder(view, position);
193        }
194    }
195
196    protected static class TextPagerAdapter extends BasePagerAdapter<String> {
197        @Override
198        public Object instantiateItem(ViewGroup container, int position) {
199            final TextView view = new TextView(container.getContext());
200            view.setText(mEntries.get(position).second);
201            configureInstantiatedItem(view, position);
202
203            // Unlike ListView adapters, the ViewPager adapter is responsible
204            // for adding the view to the container.
205            container.addView(view);
206
207            return new ViewHolder(view, position);
208        }
209    }
210
211    protected static class ButtonPagerAdapter extends BasePagerAdapter<Integer> {
212        private ArrayList<Button[]> mButtons = new ArrayList<>();
213
214        @Override
215        public void add(String title, Integer content) {
216            super.add(title, content);
217            mButtons.add(new Button[3]);
218        }
219
220        @Override
221        public Object instantiateItem(ViewGroup container, int position) {
222            final LinearLayout view = new LinearLayout(container.getContext());
223            view.setBackgroundColor(mEntries.get(position).second);
224            view.setOrientation(LinearLayout.HORIZONTAL);
225            configureInstantiatedItem(view, position);
226
227            for (int i = 0; i < 3; ++i) {
228                Button but = new Button(container.getContext());
229                but.setText("" + i);
230                but.setFocusableInTouchMode(true);
231                view.addView(but, ViewGroup.LayoutParams.WRAP_CONTENT,
232                        ViewGroup.LayoutParams.WRAP_CONTENT);
233                mButtons.get(position)[i] = but;
234            }
235
236            // Unlike ListView adapters, the ViewPager adapter is responsible
237            // for adding the view to the container.
238            container.addView(view);
239
240            return new ViewHolder(view, position);
241        }
242
243        public View getButton(int page, int idx) {
244            return mButtons.get(page)[idx];
245        }
246    }
247
248    public BaseViewPagerTest(Class<T> activityClass) {
249        super(activityClass);
250    }
251
252    @Before
253    public void setUp() throws Exception {
254        final T activity = mActivityTestRule.getActivity();
255        mViewPager = (ViewPager) activity.findViewById(R.id.pager);
256
257        ColorPagerAdapter adapter = new ColorPagerAdapter();
258        adapter.add("Red", Color.RED);
259        adapter.add("Green", Color.GREEN);
260        adapter.add("Blue", Color.BLUE);
261        onView(withId(R.id.pager)).perform(setAdapter(adapter), scrollToPage(0, false));
262    }
263
264    @After
265    public void tearDown() throws Exception {
266        onView(withId(R.id.pager)).perform(setAdapter(null));
267    }
268
269    private void verifyPageSelections(boolean smoothScroll) {
270        assertEquals("Initial state", 0, mViewPager.getCurrentItem());
271
272        ViewPager.OnPageChangeListener mockPageChangeListener =
273                mock(ViewPager.OnPageChangeListener.class);
274        mViewPager.addOnPageChangeListener(mockPageChangeListener);
275
276        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
277        assertEquals("Scroll right", 1, mViewPager.getCurrentItem());
278        verify(mockPageChangeListener, times(1)).onPageSelected(1);
279
280        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
281        assertEquals("Scroll right", 2, mViewPager.getCurrentItem());
282        verify(mockPageChangeListener, times(1)).onPageSelected(2);
283
284        // Try "scrolling" beyond the last page and test that we're still on the last page.
285        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
286        assertEquals("Scroll right beyond last page", 2, mViewPager.getCurrentItem());
287        // We're still on this page, so we shouldn't have been called again with index 2
288        verify(mockPageChangeListener, times(1)).onPageSelected(2);
289
290        onView(withId(R.id.pager)).perform(scrollLeft(smoothScroll));
291        assertEquals("Scroll left", 1, mViewPager.getCurrentItem());
292        // Verify that this is the second time we're called on index 1
293        verify(mockPageChangeListener, times(2)).onPageSelected(1);
294
295        onView(withId(R.id.pager)).perform(scrollLeft(smoothScroll));
296        assertEquals("Scroll left", 0, mViewPager.getCurrentItem());
297        // Verify that this is the first time we're called on index 0
298        verify(mockPageChangeListener, times(1)).onPageSelected(0);
299
300        // Try "scrolling" beyond the first page and test that we're still on the first page.
301        onView(withId(R.id.pager)).perform(scrollLeft(smoothScroll));
302        assertEquals("Scroll left beyond first page", 0, mViewPager.getCurrentItem());
303        // We're still on this page, so we shouldn't have been called again with index 0
304        verify(mockPageChangeListener, times(1)).onPageSelected(0);
305
306        // Unregister our listener
307        mViewPager.removeOnPageChangeListener(mockPageChangeListener);
308
309        // Go from index 0 to index 2
310        onView(withId(R.id.pager)).perform(scrollToPage(2, smoothScroll));
311        assertEquals("Scroll to last page", 2, mViewPager.getCurrentItem());
312        // Our listener is not registered anymore, so we shouldn't have been called with index 2
313        verify(mockPageChangeListener, times(1)).onPageSelected(2);
314
315        // And back to 0
316        onView(withId(R.id.pager)).perform(scrollToPage(0, smoothScroll));
317        assertEquals("Scroll to first page", 0, mViewPager.getCurrentItem());
318        // Our listener is not registered anymore, so we shouldn't have been called with index 0
319        verify(mockPageChangeListener, times(1)).onPageSelected(0);
320
321        // Verify the overall sequence of calls to onPageSelected of our listener
322        ArgumentCaptor<Integer> pageSelectedCaptor = ArgumentCaptor.forClass(int.class);
323        verify(mockPageChangeListener, times(4)).onPageSelected(pageSelectedCaptor.capture());
324        assertThat(pageSelectedCaptor.getAllValues(), TestUtilsMatchers.matches(1, 2, 1, 0));
325    }
326
327    @Test
328    @MediumTest
329    public void testPageSelectionsImmediate() {
330        verifyPageSelections(false);
331    }
332
333    @Test
334    @LargeTest
335    public void testPageSelectionsSmooth() {
336        verifyPageSelections(true);
337    }
338
339    private void verifyPageChangeViewActions(ViewAction next, ViewAction previous) {
340        assertEquals("Initial state", 0, mViewPager.getCurrentItem());
341        assertFalse(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
342        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
343
344        ViewPager.OnPageChangeListener mockPageChangeListener =
345                mock(ViewPager.OnPageChangeListener.class);
346        mViewPager.addOnPageChangeListener(mockPageChangeListener);
347
348        onView(withId(R.id.pager)).perform(next);
349        assertEquals("Move to next page", 1, mViewPager.getCurrentItem());
350        verify(mockPageChangeListener, times(1)).onPageSelected(1);
351        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
352        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
353
354        onView(withId(R.id.pager)).perform(next);
355        assertEquals("Move to next page", 2, mViewPager.getCurrentItem());
356        verify(mockPageChangeListener, times(1)).onPageSelected(2);
357        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
358        assertFalse(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
359
360        // Try swiping beyond the last page and test that we're still on the last page.
361        onView(withId(R.id.pager)).perform(next);
362        assertEquals("Attempt to move to next page beyond last page", 2,
363                mViewPager.getCurrentItem());
364        // We're still on this page, so we shouldn't have been called again with index 2
365        verify(mockPageChangeListener, times(1)).onPageSelected(2);
366        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
367        assertFalse(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
368
369        onView(withId(R.id.pager)).perform(previous);
370        assertEquals("Move to previous page", 1, mViewPager.getCurrentItem());
371        // Verify that this is the second time we're called on index 1
372        verify(mockPageChangeListener, times(2)).onPageSelected(1);
373        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
374        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
375
376        onView(withId(R.id.pager)).perform(previous);
377        assertEquals("Move to previous page", 0, mViewPager.getCurrentItem());
378        // Verify that this is the first time we're called on index 0
379        verify(mockPageChangeListener, times(1)).onPageSelected(0);
380        assertFalse(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
381        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
382
383        // Try swiping beyond the first page and test that we're still on the first page.
384        onView(withId(R.id.pager)).perform(previous);
385        assertEquals("Attempt to move to previous page beyond first page", 0,
386                mViewPager.getCurrentItem());
387        // We're still on this page, so we shouldn't have been called again with index 0
388        verify(mockPageChangeListener, times(1)).onPageSelected(0);
389        assertFalse(mViewPager.canScrollHorizontally(DIRECTION_LEFT));
390        assertTrue(mViewPager.canScrollHorizontally(DIRECTION_RIGHT));
391
392        mViewPager.removeOnPageChangeListener(mockPageChangeListener);
393
394        // Verify the overall sequence of calls to onPageSelected of our listener
395        ArgumentCaptor<Integer> pageSelectedCaptor = ArgumentCaptor.forClass(int.class);
396        verify(mockPageChangeListener, times(4)).onPageSelected(pageSelectedCaptor.capture());
397        assertThat(pageSelectedCaptor.getAllValues(), TestUtilsMatchers.matches(1, 2, 1, 0));
398    }
399
400    @Test
401    @LargeTest
402    public void testPageSwipes() {
403        verifyPageChangeViewActions(wrap(swipeLeft()), wrap(swipeRight()));
404    }
405
406    @Test
407    @MediumTest
408    public void testArrowPageChanges() {
409        verifyPageChangeViewActions(arrowScroll(View.FOCUS_RIGHT), arrowScroll(View.FOCUS_LEFT));
410    }
411
412    @Test
413    @LargeTest
414    public void testPageSwipesComposite() {
415        assertEquals("Initial state", 0, mViewPager.getCurrentItem());
416
417        onView(withId(R.id.pager)).perform(wrap(swipeLeft()), wrap(swipeLeft()));
418        assertEquals("Swipe twice left", 2, mViewPager.getCurrentItem());
419
420        onView(withId(R.id.pager)).perform(wrap(swipeLeft()), wrap(swipeRight()));
421        assertEquals("Swipe left beyond last page and then right", 1, mViewPager.getCurrentItem());
422
423        onView(withId(R.id.pager)).perform(wrap(swipeRight()), wrap(swipeRight()));
424        assertEquals("Swipe right and then right beyond first page", 0,
425                mViewPager.getCurrentItem());
426
427        onView(withId(R.id.pager)).perform(wrap(swipeRight()), wrap(swipeLeft()));
428        assertEquals("Swipe right beyond first page and then left", 1, mViewPager.getCurrentItem());
429    }
430
431    private void verifyPageContent(boolean smoothScroll) {
432        assertEquals("Initial state", 0, mViewPager.getCurrentItem());
433
434        // Verify the displayed content to match the initial adapter - with 3 pages and each
435        // one rendered as a View.
436
437        // Page #0 should be displayed, page #1 should not be displayed and page #2 should not exist
438        // yet as it's outside of the offscreen window limit.
439        onView(withId(R.id.page_0)).check(matches(allOf(
440                isOfClass(View.class),
441                isDisplayed(),
442                backgroundColor(Color.RED))));
443        onView(withId(R.id.page_1)).check(matches(not(isDisplayed())));
444        onView(withId(R.id.page_2)).check(doesNotExist());
445
446        // Scroll one page to select page #1
447        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
448        assertEquals("Scroll right", 1, mViewPager.getCurrentItem());
449        // Pages #0 / #2 should not be displayed, page #1 should be displayed.
450        onView(withId(R.id.page_0)).check(matches(not(isDisplayed())));
451        onView(withId(R.id.page_1)).check(matches(allOf(
452                isOfClass(View.class),
453                isDisplayed(),
454                backgroundColor(Color.GREEN))));
455        onView(withId(R.id.page_2)).check(matches(not(isDisplayed())));
456
457        // Scroll one more page to select page #2
458        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
459        assertEquals("Scroll right again", 2, mViewPager.getCurrentItem());
460        // Page #0 should not exist as it's bumped to the outside of the offscreen window limit,
461        // page #1 should not be displayed, page #2 should be displayed.
462        onView(withId(R.id.page_0)).check(doesNotExist());
463        onView(withId(R.id.page_1)).check(matches(not(isDisplayed())));
464        onView(withId(R.id.page_2)).check(matches(allOf(
465                isOfClass(View.class),
466                isDisplayed(),
467                backgroundColor(Color.BLUE))));
468    }
469
470    @Test
471    @MediumTest
472    public void testPageContentImmediate() {
473        verifyPageContent(false);
474    }
475
476    @Test
477    @LargeTest
478    public void testPageContentSmooth() {
479        verifyPageContent(true);
480    }
481
482    private void verifyAdapterChange(boolean smoothScroll) {
483        // Verify that we have the expected initial adapter
484        PagerAdapter initialAdapter = mViewPager.getAdapter();
485        assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass());
486        assertEquals("Initial adapter page count", 3, initialAdapter.getCount());
487
488        // Create a new adapter
489        TextPagerAdapter newAdapter = new TextPagerAdapter();
490        newAdapter.add("Title 0", "Body 0");
491        newAdapter.add("Title 1", "Body 1");
492        newAdapter.add("Title 2", "Body 2");
493        newAdapter.add("Title 3", "Body 3");
494        onView(withId(R.id.pager)).perform(setAdapter(newAdapter), scrollToPage(0, smoothScroll));
495
496        // Verify the displayed content to match the newly set adapter - with 4 pages and each
497        // one rendered as a TextView.
498
499        // Page #0 should be displayed, page #1 should not be displayed and pages #2 / #3 should not
500        // exist yet as they're outside of the offscreen window limit.
501        onView(withId(R.id.page_0)).check(matches(allOf(
502                isOfClass(TextView.class),
503                isDisplayed(),
504                withText("Body 0"))));
505        onView(withId(R.id.page_1)).check(matches(not(isDisplayed())));
506        onView(withId(R.id.page_2)).check(doesNotExist());
507        onView(withId(R.id.page_3)).check(doesNotExist());
508
509        // Scroll one page to select page #1
510        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
511        assertEquals("Scroll right", 1, mViewPager.getCurrentItem());
512        // Pages #0 / #2 should not be displayed, page #1 should be displayed, page #3 is still
513        // outside the offscreen limit.
514        onView(withId(R.id.page_0)).check(matches(not(isDisplayed())));
515        onView(withId(R.id.page_1)).check(matches(allOf(
516                isOfClass(TextView.class),
517                isDisplayed(),
518                withText("Body 1"))));
519        onView(withId(R.id.page_2)).check(matches(not(isDisplayed())));
520        onView(withId(R.id.page_3)).check(doesNotExist());
521
522        // Scroll one more page to select page #2
523        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
524        assertEquals("Scroll right again", 2, mViewPager.getCurrentItem());
525        // Page #0 should not exist as it's bumped to the outside of the offscreen window limit,
526        // pages #1 / #3 should not be displayed, page #2 should be displayed.
527        onView(withId(R.id.page_0)).check(doesNotExist());
528        onView(withId(R.id.page_1)).check(matches(not(isDisplayed())));
529        onView(withId(R.id.page_2)).check(matches(allOf(
530                isOfClass(TextView.class),
531                isDisplayed(),
532                withText("Body 2"))));
533        onView(withId(R.id.page_3)).check(matches(not(isDisplayed())));
534
535        // Scroll one more page to select page #2
536        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
537        assertEquals("Scroll right one more time", 3, mViewPager.getCurrentItem());
538        // Pages #0 / #1 should not exist as they're bumped to the outside of the offscreen window
539        // limit, page #2 should not be displayed, page #3 should be displayed.
540        onView(withId(R.id.page_0)).check(doesNotExist());
541        onView(withId(R.id.page_1)).check(doesNotExist());
542        onView(withId(R.id.page_2)).check(matches(not(isDisplayed())));
543        onView(withId(R.id.page_3)).check(matches(allOf(
544                isOfClass(TextView.class),
545                isDisplayed(),
546                withText("Body 3"))));
547    }
548
549    @Test
550    @MediumTest
551    public void testAdapterChangeImmediate() {
552        verifyAdapterChange(false);
553    }
554
555    @Test
556    @LargeTest
557    public void testAdapterChangeSmooth() {
558        verifyAdapterChange(true);
559    }
560
561    private void verifyTitleStripLayout(String expectedStartTitle, String expectedSelectedTitle,
562            String expectedEndTitle, int selectedPageId) {
563        // Check that the title strip spans the whole width of the pager and is aligned to
564        // its top
565        onView(withId(R.id.titles)).check(isLeftAlignedWith(withId(R.id.pager)));
566        onView(withId(R.id.titles)).check(isRightAlignedWith(withId(R.id.pager)));
567        onView(withId(R.id.titles)).check(isTopAlignedWith(withId(R.id.pager)));
568
569        // Check that the currently selected page spans the whole width of the pager and is below
570        // the title strip
571        onView(withId(selectedPageId)).check(isLeftAlignedWith(withId(R.id.pager)));
572        onView(withId(selectedPageId)).check(isRightAlignedWith(withId(R.id.pager)));
573        onView(withId(selectedPageId)).check(isBelow(withId(R.id.titles)));
574        onView(withId(selectedPageId)).check(isBottomAlignedWith(withId(R.id.pager)));
575
576        boolean hasStartTitle = !TextUtils.isEmpty(expectedStartTitle);
577        boolean hasEndTitle = !TextUtils.isEmpty(expectedEndTitle);
578
579        // Check that the title strip shows the expected number of children (tab titles)
580        int nonNullTitles = (hasStartTitle ? 1 : 0) + 1 + (hasEndTitle ? 1 : 0);
581        onView(withId(R.id.titles)).check(hasDisplayedChildren(nonNullTitles));
582
583        if (hasStartTitle) {
584            // Check that the title for the start page is displayed at the start edge of its parent
585            // (title strip)
586            onView(withId(R.id.titles)).check(matches(hasDescendant(
587                    allOf(withText(expectedStartTitle), isDisplayed(), startAlignedToParent()))));
588        }
589        // Check that the title for the selected page is displayed centered in its parent
590        // (title strip)
591        onView(withId(R.id.titles)).check(matches(hasDescendant(
592                allOf(withText(expectedSelectedTitle), isDisplayed(), centerAlignedInParent()))));
593        if (hasEndTitle) {
594            // Check that the title for the end page is displayed at the end edge of its parent
595            // (title strip)
596            onView(withId(R.id.titles)).check(matches(hasDescendant(
597                    allOf(withText(expectedEndTitle), isDisplayed(), endAlignedToParent()))));
598        }
599    }
600
601    private void verifyPagerStrip(boolean smoothScroll) {
602        // Set an adapter with 5 pages
603        final ColorPagerAdapter adapter = new ColorPagerAdapter();
604        adapter.add("Red", Color.RED);
605        adapter.add("Green", Color.GREEN);
606        adapter.add("Blue", Color.BLUE);
607        adapter.add("Yellow", Color.YELLOW);
608        adapter.add("Magenta", Color.MAGENTA);
609        onView(withId(R.id.pager)).perform(setAdapter(adapter),
610                scrollToPage(0, smoothScroll));
611
612        // Check that the pager has a title strip
613        onView(withId(R.id.pager)).check(matches(hasDescendant(withId(R.id.titles))));
614        // Check that the title strip is displayed and is of the expected class
615        onView(withId(R.id.titles)).check(matches(allOf(
616                isDisplayed(), isOfClass(getStripClass()))));
617
618        // The following block tests the overall layout of tab strip and main pager content
619        // (vertical stacking), the content of the tab strip (showing texts for the selected
620        // tab and the ones on its left / right) as well as the alignment of the content in the
621        // tab strip (selected in center, others on left and right).
622
623        // Check the content and alignment of title strip for selected page #0
624        verifyTitleStripLayout(null, "Red", "Green", R.id.page_0);
625
626        // Scroll one page to select page #1 and check layout / content of title strip
627        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
628        verifyTitleStripLayout("Red", "Green", "Blue", R.id.page_1);
629
630        // Scroll one page to select page #2 and check layout / content of title strip
631        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
632        verifyTitleStripLayout("Green", "Blue", "Yellow", R.id.page_2);
633
634        // Scroll one page to select page #3 and check layout / content of title strip
635        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
636        verifyTitleStripLayout("Blue", "Yellow", "Magenta", R.id.page_3);
637
638        // Scroll one page to select page #4 and check layout / content of title strip
639        onView(withId(R.id.pager)).perform(scrollRight(smoothScroll));
640        verifyTitleStripLayout("Yellow", "Magenta", null, R.id.page_4);
641
642        // Scroll back to page #0
643        onView(withId(R.id.pager)).perform(scrollToPage(0, smoothScroll));
644
645        assertStripInteraction(smoothScroll);
646    }
647
648    @Test
649    @MediumTest
650    public void testPagerStripImmediate() {
651        verifyPagerStrip(false);
652    }
653
654    @Test
655    @LargeTest
656    public void testPagerStripSmooth() {
657        verifyPagerStrip(true);
658    }
659
660    /**
661     * Returns the class of the pager strip.
662     */
663    protected abstract Class getStripClass();
664
665    /**
666     * Checks assertions that are specific to the pager strip implementation (interactive or
667     * non interactive).
668     */
669    protected abstract void assertStripInteraction(boolean smoothScroll);
670
671    /**
672     * Helper method that performs the specified action on the <code>ViewPager</code> and then
673     * checks the sequence of calls to the page change listener based on the specified expected
674     * scroll state changes.
675     *
676     * If that expected list is empty, this method verifies that there were no calls to
677     * onPageScrollStateChanged when the action was performed. Otherwise it verifies that the actual
678     * sequence of calls to onPageScrollStateChanged matches the expected (specified) one.
679     */
680    private void verifyScrollStateChange(ViewAction viewAction, int... expectedScrollStateChanges) {
681        ViewPager.OnPageChangeListener mockPageChangeListener =
682                mock(ViewPager.OnPageChangeListener.class);
683        mViewPager.addOnPageChangeListener(mockPageChangeListener);
684
685        // Perform our action
686        onView(withId(R.id.pager)).perform(viewAction);
687
688        int expectedScrollStateChangeCount = (expectedScrollStateChanges != null) ?
689                expectedScrollStateChanges.length : 0;
690
691        if (expectedScrollStateChangeCount == 0) {
692            verify(mockPageChangeListener, never()).onPageScrollStateChanged(anyInt());
693        } else {
694            ArgumentCaptor<Integer> pageScrollStateCaptor = ArgumentCaptor.forClass(int.class);
695            verify(mockPageChangeListener, times(expectedScrollStateChangeCount)).
696                    onPageScrollStateChanged(pageScrollStateCaptor.capture());
697            assertThat(pageScrollStateCaptor.getAllValues(),
698                    TestUtilsMatchers.matches(expectedScrollStateChanges));
699        }
700
701        // Remove our mock listener to get back to clean state for the next test
702        mViewPager.removeOnPageChangeListener(mockPageChangeListener);
703    }
704
705    @Test
706    @MediumTest
707    public void testPageScrollStateChangedImmediate() {
708        // Note that all the actions tested in this method are immediate (no scrolling) and
709        // as such we test that we do not get any calls to onPageScrollStateChanged in any of them
710
711        // Select one page to the right
712        verifyScrollStateChange(scrollRight(false));
713        // Select one more page to the right
714        verifyScrollStateChange(scrollRight(false));
715        // Select one page to the left
716        verifyScrollStateChange(scrollLeft(false));
717        // Select one more page to the left
718        verifyScrollStateChange(scrollLeft(false));
719        // Select last page
720        verifyScrollStateChange(scrollToLast(false));
721        // Select first page
722        verifyScrollStateChange(scrollToFirst(false));
723    }
724
725    @Test
726    @MediumTest
727    public void testPageScrollStateChangedSmooth() {
728        // Note that all the actions tested in this method use smooth scrolling and as such we test
729        // that we get the matching calls to onPageScrollStateChanged
730        final int[] expectedScrollStateChanges = new int[] {
731                ViewPager.SCROLL_STATE_SETTLING, ViewPager.SCROLL_STATE_IDLE
732        };
733
734        // Select one page to the right
735        verifyScrollStateChange(scrollRight(true), expectedScrollStateChanges);
736        // Select one more page to the right
737        verifyScrollStateChange(scrollRight(true), expectedScrollStateChanges);
738        // Select one page to the left
739        verifyScrollStateChange(scrollLeft(true), expectedScrollStateChanges);
740        // Select one more page to the left
741        verifyScrollStateChange(scrollLeft(true), expectedScrollStateChanges);
742        // Select last page
743        verifyScrollStateChange(scrollToLast(true), expectedScrollStateChanges);
744        // Select first page
745        verifyScrollStateChange(scrollToFirst(true), expectedScrollStateChanges);
746    }
747
748    @Test
749    @MediumTest
750    public void testPageScrollStateChangedSwipe() {
751        // Note that all the actions tested in this method use swiping and as such we test
752        // that we get the matching calls to onPageScrollStateChanged
753        final int[] expectedScrollStateChanges = new int[] { ViewPager.SCROLL_STATE_DRAGGING,
754                ViewPager.SCROLL_STATE_SETTLING, ViewPager.SCROLL_STATE_IDLE };
755
756        // Swipe one page to the left
757        verifyScrollStateChange(wrap(swipeLeft()), expectedScrollStateChanges);
758        assertEquals("Swipe left", 1, mViewPager.getCurrentItem());
759
760        // Swipe one more page to the left
761        verifyScrollStateChange(wrap(swipeLeft()), expectedScrollStateChanges);
762        assertEquals("Swipe left", 2, mViewPager.getCurrentItem());
763
764        // Swipe one page to the right
765        verifyScrollStateChange(wrap(swipeRight()), expectedScrollStateChanges);
766        assertEquals("Swipe right", 1, mViewPager.getCurrentItem());
767
768        // Swipe one more page to the right
769        verifyScrollStateChange(wrap(swipeRight()), expectedScrollStateChanges);
770        assertEquals("Swipe right", 0, mViewPager.getCurrentItem());
771    }
772
773    /**
774     * Helper method to verify the internal consistency of values passed to
775     * {@link ViewPager.OnPageChangeListener#onPageScrolled} callback when we go from a page with
776     * lower index to a page with higher index.
777     *
778     * @param startPageIndex Index of the starting page.
779     * @param endPageIndex Index of the ending page.
780     * @param pageWidth Page width in pixels.
781     * @param positions List of "position" values passed to all
782     *      {@link ViewPager.OnPageChangeListener#onPageScrolled} calls.
783     * @param positionOffsets List of "positionOffset" values passed to all
784     *      {@link ViewPager.OnPageChangeListener#onPageScrolled} calls.
785     * @param positionOffsetPixels List of "positionOffsetPixel" values passed to all
786     *      {@link ViewPager.OnPageChangeListener#onPageScrolled} calls.
787     */
788    private void verifyScrollCallbacksToHigherPage(int startPageIndex, int endPageIndex,
789            int pageWidth, List<Integer> positions, List<Float> positionOffsets,
790            List<Integer> positionOffsetPixels) {
791        int callbackCount = positions.size();
792
793        // The last entry in all three lists must match the index of the end page
794        Assert.assertEquals("Position at last index",
795                endPageIndex, (int) positions.get(callbackCount - 1));
796        Assert.assertEquals("Position offset at last index",
797                0.0f, positionOffsets.get(callbackCount - 1), 0.0f);
798        Assert.assertEquals("Position offset pixel at last index",
799                0, (int) positionOffsetPixels.get(callbackCount - 1));
800
801        // If this was our only callback, return. This can happen on immediate page change
802        // or on very slow devices.
803        if (callbackCount == 1) {
804            return;
805        }
806
807        // If we have additional callbacks, verify that the values provided to our callback reflect
808        // a valid sequence of events going from startPageIndex to endPageIndex.
809        for (int i = 0; i < callbackCount - 1; i++) {
810            // Page position must be between start page and end page
811            int pagePositionCurr = positions.get(i);
812            if ((pagePositionCurr < startPageIndex) || (pagePositionCurr > endPageIndex)) {
813                Assert.fail("Position at #" + i + " is " + pagePositionCurr +
814                        ", but should be between " + startPageIndex + " and " + endPageIndex);
815            }
816
817            // Page position sequence cannot be decreasing
818            int pagePositionNext = positions.get(i + 1);
819            if (pagePositionCurr > pagePositionNext) {
820                Assert.fail("Position at #" + i + " is " + pagePositionCurr +
821                        " and then decreases to " + pagePositionNext + " at #" + (i + 1));
822            }
823
824            // Position offset must be in [0..1) range (inclusive / exclusive)
825            float positionOffsetCurr = positionOffsets.get(i);
826            if ((positionOffsetCurr < 0.0f) || (positionOffsetCurr >= 1.0f)) {
827                Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr +
828                        ", but should be in [0..1) range");
829            }
830
831            // Position pixel offset must be in [0..pageWidth) range (inclusive / exclusive)
832            int positionOffsetPixelCurr = positionOffsetPixels.get(i);
833            if ((positionOffsetPixelCurr < 0.0f) || (positionOffsetPixelCurr >= pageWidth)) {
834                Assert.fail("Position pixel offset at #" + i + " is " + positionOffsetCurr +
835                        ", but should be in [0.." + pageWidth + ") range");
836            }
837
838            // Position pixel offset must match the position offset and page width within
839            // a one-pixel tolerance range
840            Assert.assertEquals("Position pixel offset at #" + i + " is " +
841                    positionOffsetPixelCurr + ", but doesn't match position offset which is" +
842                    positionOffsetCurr + " and page width which is " + pageWidth,
843                    positionOffsetPixelCurr, positionOffsetCurr * pageWidth, 1.0f);
844
845            // If we stay on the same page between this index and the next one, both position
846            // offset and position pixel offset must increase
847            if (pagePositionNext == pagePositionCurr) {
848                float positionOffsetNext = positionOffsets.get(i + 1);
849                // Note that since position offset sequence is float, we are checking for strict
850                // increasing
851                if (positionOffsetNext <= positionOffsetCurr) {
852                    Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr +
853                            " and at #" + (i + 1) + " is " + positionOffsetNext +
854                            ". Since both are for page " + pagePositionCurr +
855                            ", they cannot decrease");
856                }
857
858                int positionOffsetPixelNext = positionOffsetPixels.get(i + 1);
859                // Note that since position offset pixel sequence is the mapping of position offset
860                // into screen pixels, we can get two (or more) callbacks with strictly increasing
861                // position offsets that are converted into the same pixel value. This is why here
862                // we are checking for non-strict increasing
863                if (positionOffsetPixelNext < positionOffsetPixelCurr) {
864                    Assert.fail("Position offset pixel at #" + i + " is " +
865                            positionOffsetPixelCurr + " and at #" + (i + 1) + " is " +
866                            positionOffsetPixelNext + ". Since both are for page " +
867                            pagePositionCurr + ", they cannot decrease");
868                }
869            }
870        }
871    }
872
873    /**
874     * Helper method to verify the internal consistency of values passed to
875     * {@link ViewPager.OnPageChangeListener#onPageScrolled} callback when we go from a page with
876     * higher index to a page with lower index.
877     *
878     * @param startPageIndex Index of the starting page.
879     * @param endPageIndex Index of the ending page.
880     * @param pageWidth Page width in pixels.
881     * @param positions List of "position" values passed to all
882     *      {@link ViewPager.OnPageChangeListener#onPageScrolled} calls.
883     * @param positionOffsets List of "positionOffset" values passed to all
884     *      {@link ViewPager.OnPageChangeListener#onPageScrolled} calls.
885     * @param positionOffsetPixels List of "positionOffsetPixel" values passed to all
886     *      {@link ViewPager.OnPageChangeListener#onPageScrolled} calls.
887     */
888    private void verifyScrollCallbacksToLowerPage(int startPageIndex, int endPageIndex,
889            int pageWidth, List<Integer> positions, List<Float> positionOffsets,
890            List<Integer> positionOffsetPixels) {
891        int callbackCount = positions.size();
892
893        // The last entry in all three lists must match the index of the end page
894        Assert.assertEquals("Position at last index",
895                endPageIndex, (int) positions.get(callbackCount - 1));
896        Assert.assertEquals("Position offset at last index",
897                0.0f, positionOffsets.get(callbackCount - 1), 0.0f);
898        Assert.assertEquals("Position offset pixel at last index",
899                0, (int) positionOffsetPixels.get(callbackCount - 1));
900
901        // If this was our only callback, return. This can happen on immediate page change
902        // or on very slow devices.
903        if (callbackCount == 1) {
904            return;
905        }
906
907        // If we have additional callbacks, verify that the values provided to our callback reflect
908        // a valid sequence of events going from startPageIndex to endPageIndex.
909        for (int i = 0; i < callbackCount - 1; i++) {
910            // Page position must be between start page and end page
911            int pagePositionCurr = positions.get(i);
912            if ((pagePositionCurr > startPageIndex) || (pagePositionCurr < endPageIndex)) {
913                Assert.fail("Position at #" + i + " is " + pagePositionCurr +
914                        ", but should be between " + endPageIndex + " and " + startPageIndex);
915            }
916
917            // Page position sequence cannot be increasing
918            int pagePositionNext = positions.get(i + 1);
919            if (pagePositionCurr < pagePositionNext) {
920                Assert.fail("Position at #" + i + " is " + pagePositionCurr +
921                        " and then increases to " + pagePositionNext + " at #" + (i + 1));
922            }
923
924            // Position offset must be in [0..1) range (inclusive / exclusive)
925            float positionOffsetCurr = positionOffsets.get(i);
926            if ((positionOffsetCurr < 0.0f) || (positionOffsetCurr >= 1.0f)) {
927                Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr +
928                        ", but should be in [0..1) range");
929            }
930
931            // Position pixel offset must be in [0..pageWidth) range (inclusive / exclusive)
932            int positionOffsetPixelCurr = positionOffsetPixels.get(i);
933            if ((positionOffsetPixelCurr < 0.0f) || (positionOffsetPixelCurr >= pageWidth)) {
934                Assert.fail("Position pixel offset at #" + i + " is " + positionOffsetCurr +
935                        ", but should be in [0.." + pageWidth + ") range");
936            }
937
938            // Position pixel offset must match the position offset and page width within
939            // a one-pixel tolerance range
940            Assert.assertEquals("Position pixel offset at #" + i + " is " +
941                            positionOffsetPixelCurr + ", but doesn't match position offset which is" +
942                            positionOffsetCurr + " and page width which is " + pageWidth,
943                    positionOffsetPixelCurr, positionOffsetCurr * pageWidth, 1.0f);
944
945            // If we stay on the same page between this index and the next one, both position
946            // offset and position pixel offset must decrease
947            if (pagePositionNext == pagePositionCurr) {
948                float positionOffsetNext = positionOffsets.get(i + 1);
949                // Note that since position offset sequence is float, we are checking for strict
950                // decreasing
951                if (positionOffsetNext >= positionOffsetCurr) {
952                    Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr +
953                            " and at #" + (i + 1) + " is " + positionOffsetNext +
954                            ". Since both are for page " + pagePositionCurr +
955                            ", they cannot increase");
956                }
957
958                int positionOffsetPixelNext = positionOffsetPixels.get(i + 1);
959                // Note that since position offset pixel sequence is the mapping of position offset
960                // into screen pixels, we can get two (or more) callbacks with strictly decreasing
961                // position offsets that are converted into the same pixel value. This is why here
962                // we are checking for non-strict decreasing
963                if (positionOffsetPixelNext > positionOffsetPixelCurr) {
964                    Assert.fail("Position offset pixel at #" + i + " is " +
965                            positionOffsetPixelCurr + " and at #" + (i + 1) + " is " +
966                            positionOffsetPixelNext + ". Since both are for page " +
967                            pagePositionCurr + ", they cannot increase");
968                }
969            }
970        }
971    }
972
973    private void verifyScrollCallbacksToHigherPage(ViewAction viewAction,
974            int expectedEndPageIndex) {
975        final int startPageIndex = mViewPager.getCurrentItem();
976
977        ViewPager.OnPageChangeListener mockPageChangeListener =
978                mock(ViewPager.OnPageChangeListener.class);
979        mViewPager.addOnPageChangeListener(mockPageChangeListener);
980
981        // Perform our action
982        onView(withId(R.id.pager)).perform(viewAction);
983
984        final int endPageIndex = mViewPager.getCurrentItem();
985        Assert.assertEquals("Current item after action", expectedEndPageIndex, endPageIndex);
986
987        ArgumentCaptor<Integer> positionCaptor = ArgumentCaptor.forClass(int.class);
988        ArgumentCaptor<Float> positionOffsetCaptor = ArgumentCaptor.forClass(float.class);
989        ArgumentCaptor<Integer> positionOffsetPixelsCaptor = ArgumentCaptor.forClass(int.class);
990        verify(mockPageChangeListener, atLeastOnce()).onPageScrolled(positionCaptor.capture(),
991                positionOffsetCaptor.capture(), positionOffsetPixelsCaptor.capture());
992
993        verifyScrollCallbacksToHigherPage(startPageIndex, endPageIndex, mViewPager.getWidth(),
994                positionCaptor.getAllValues(), positionOffsetCaptor.getAllValues(),
995                positionOffsetPixelsCaptor.getAllValues());
996
997        // Remove our mock listener to get back to clean state for the next test
998        mViewPager.removeOnPageChangeListener(mockPageChangeListener);
999    }
1000
1001    private void verifyScrollCallbacksToLowerPage(ViewAction viewAction,
1002            int expectedEndPageIndex) {
1003        final int startPageIndex = mViewPager.getCurrentItem();
1004
1005        ViewPager.OnPageChangeListener mockPageChangeListener =
1006                mock(ViewPager.OnPageChangeListener.class);
1007        mViewPager.addOnPageChangeListener(mockPageChangeListener);
1008
1009        // Perform our action
1010        onView(withId(R.id.pager)).perform(viewAction);
1011
1012        final int endPageIndex = mViewPager.getCurrentItem();
1013        Assert.assertEquals("Current item after action", expectedEndPageIndex, endPageIndex);
1014
1015        ArgumentCaptor<Integer> positionCaptor = ArgumentCaptor.forClass(int.class);
1016        ArgumentCaptor<Float> positionOffsetCaptor = ArgumentCaptor.forClass(float.class);
1017        ArgumentCaptor<Integer> positionOffsetPixelsCaptor = ArgumentCaptor.forClass(int.class);
1018        verify(mockPageChangeListener, atLeastOnce()).onPageScrolled(positionCaptor.capture(),
1019                positionOffsetCaptor.capture(), positionOffsetPixelsCaptor.capture());
1020
1021        verifyScrollCallbacksToLowerPage(startPageIndex, endPageIndex, mViewPager.getWidth(),
1022                positionCaptor.getAllValues(), positionOffsetCaptor.getAllValues(),
1023                positionOffsetPixelsCaptor.getAllValues());
1024
1025        // Remove our mock listener to get back to clean state for the next test
1026        mViewPager.removeOnPageChangeListener(mockPageChangeListener);
1027    }
1028
1029    @Test
1030    @MediumTest
1031    public void testPageScrollPositionChangesImmediate() {
1032        // Scroll one page to the right
1033        verifyScrollCallbacksToHigherPage(scrollRight(false), 1);
1034        // Scroll one more page to the right
1035        verifyScrollCallbacksToHigherPage(scrollRight(false), 2);
1036        // Scroll one page to the left
1037        verifyScrollCallbacksToLowerPage(scrollLeft(false), 1);
1038        // Scroll one more page to the left
1039        verifyScrollCallbacksToLowerPage(scrollLeft(false), 0);
1040
1041        // Scroll to the last page
1042        verifyScrollCallbacksToHigherPage(scrollToLast(false), 2);
1043        // Scroll to the first page
1044        verifyScrollCallbacksToLowerPage(scrollToFirst(false), 0);
1045    }
1046
1047    @Test
1048    @MediumTest
1049    public void testPageScrollPositionChangesSmooth() {
1050        // Scroll one page to the right
1051        verifyScrollCallbacksToHigherPage(scrollRight(true), 1);
1052        // Scroll one more page to the right
1053        verifyScrollCallbacksToHigherPage(scrollRight(true), 2);
1054        // Scroll one page to the left
1055        verifyScrollCallbacksToLowerPage(scrollLeft(true), 1);
1056        // Scroll one more page to the left
1057        verifyScrollCallbacksToLowerPage(scrollLeft(true), 0);
1058
1059        // Scroll to the last page
1060        verifyScrollCallbacksToHigherPage(scrollToLast(true), 2);
1061        // Scroll to the first page
1062        verifyScrollCallbacksToLowerPage(scrollToFirst(true), 0);
1063    }
1064
1065    @Test
1066    @MediumTest
1067    public void testPageScrollPositionChangesSwipe() {
1068        // Swipe one page to the left
1069        verifyScrollCallbacksToHigherPage(wrap(swipeLeft()), 1);
1070        // Swipe one more page to the left
1071        verifyScrollCallbacksToHigherPage(wrap(swipeLeft()), 2);
1072        // Swipe one page to the right
1073        verifyScrollCallbacksToLowerPage(wrap(swipeRight()), 1);
1074        // Swipe one more page to the right
1075        verifyScrollCallbacksToLowerPage(wrap(swipeRight()), 0);
1076    }
1077
1078    @Test
1079    @MediumTest
1080    public void testKeyboardNavigation() {
1081        ButtonPagerAdapter adapter = new ButtonPagerAdapter();
1082        adapter.add("Red", Color.RED);
1083        adapter.add("Green", Color.GREEN);
1084        adapter.add("Blue", Color.BLUE);
1085        onView(withId(R.id.pager)).perform(setAdapter(adapter), scrollToPage(0, false));
1086        View firstButton = adapter.getButton(0, 0);
1087        firstButton.requestFocus();
1088        assertTrue(firstButton.isFocused());
1089        assertEquals(0, mViewPager.getCurrentItem());
1090
1091        // Normal arrows should traverse contents first
1092        onView(is(firstButton)).perform(pressKey(KeyEvent.KEYCODE_DPAD_RIGHT));
1093        assertEquals(0, mViewPager.getCurrentItem());
1094        assertTrue(adapter.getButton(0, 1).isFocused());
1095
1096        // Alt arrows should change page even if there are more focusables in that direction
1097        onView(is(adapter.getButton(0, 1))).perform(pressKey(new EspressoKey.Builder()
1098                .withAltPressed(true).withKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT).build()));
1099        assertEquals(1, mViewPager.getCurrentItem());
1100        assertTrue(adapter.getButton(1, 0).isFocused());
1101
1102        // Normal arrows should change page if there are no more focusables in that direction
1103        onView(is(adapter.getButton(1, 0))).perform(pressKey(KeyEvent.KEYCODE_DPAD_LEFT));
1104        assertEquals(0, mViewPager.getCurrentItem());
1105    }
1106}
1107