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