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