| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package android.support.v4.view; |
| |
| import android.app.Activity; |
| import android.graphics.Color; |
| import android.support.coreui.test.R; |
| import android.support.test.espresso.ViewAction; |
| import android.support.v4.BaseInstrumentationTestCase; |
| import android.support.v4.testutils.TestUtilsMatchers; |
| import android.test.suitebuilder.annotation.MediumTest; |
| import android.test.suitebuilder.annotation.SmallTest; |
| import android.text.TextUtils; |
| import android.util.Pair; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.TextView; |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.mockito.ArgumentCaptor; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import static android.support.test.espresso.Espresso.onView; |
| import static android.support.test.espresso.action.ViewActions.swipeLeft; |
| import static android.support.test.espresso.action.ViewActions.swipeRight; |
| import static android.support.test.espresso.assertion.PositionAssertions.*; |
| import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; |
| import static android.support.test.espresso.assertion.ViewAssertions.matches; |
| import static android.support.test.espresso.matcher.ViewMatchers.*; |
| import static android.support.v4.testutils.TestUtilsAssertions.hasDisplayedChildren; |
| import static android.support.v4.testutils.TestUtilsMatchers.*; |
| import static android.support.v4.view.ViewPagerActions.*; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.hamcrest.Matchers.allOf; |
| import static org.hamcrest.core.IsNot.not; |
| import static org.junit.Assert.assertEquals; |
| import static org.mockito.Mockito.*; |
| |
| /** |
| * Base class for testing <code>ViewPager</code>. Most of the testing logic should be in this |
| * class as it is independent on the specific pager title implementation (interactive or non |
| * interactive). |
| * |
| * Testing logic that does depend on the specific pager title implementation is pushed into the |
| * extending classes in <code>assertStripInteraction()</code> method. |
| */ |
| public abstract class BaseViewPagerTest<T extends Activity> extends BaseInstrumentationTestCase<T> { |
| protected ViewPager mViewPager; |
| |
| protected static class BasePagerAdapter<Q> extends PagerAdapter { |
| protected ArrayList<Pair<String, Q>> mEntries = new ArrayList<>(); |
| |
| public void add(String title, Q content) { |
| mEntries.add(new Pair(title, content)); |
| } |
| |
| @Override |
| public int getCount() { |
| return mEntries.size(); |
| } |
| |
| protected void configureInstantiatedItem(View view, int position) { |
| switch (position) { |
| case 0: |
| view.setId(R.id.page_0); |
| break; |
| case 1: |
| view.setId(R.id.page_1); |
| break; |
| case 2: |
| view.setId(R.id.page_2); |
| break; |
| case 3: |
| view.setId(R.id.page_3); |
| break; |
| case 4: |
| view.setId(R.id.page_4); |
| break; |
| case 5: |
| view.setId(R.id.page_5); |
| break; |
| case 6: |
| view.setId(R.id.page_6); |
| break; |
| case 7: |
| view.setId(R.id.page_7); |
| break; |
| case 8: |
| view.setId(R.id.page_8); |
| break; |
| case 9: |
| view.setId(R.id.page_9); |
| break; |
| } |
| } |
| |
| @Override |
| public void destroyItem(ViewGroup container, int position, Object object) { |
| // The adapter is also responsible for removing the view. |
| container.removeView(((ViewHolder) object).view); |
| } |
| |
| @Override |
| public int getItemPosition(Object object) { |
| return ((ViewHolder) object).position; |
| } |
| |
| @Override |
| public boolean isViewFromObject(View view, Object object) { |
| return ((ViewHolder) object).view == view; |
| } |
| |
| @Override |
| public CharSequence getPageTitle(int position) { |
| return mEntries.get(position).first; |
| } |
| |
| protected static class ViewHolder { |
| final View view; |
| final int position; |
| |
| public ViewHolder(View view, int position) { |
| this.view = view; |
| this.position = position; |
| } |
| } |
| } |
| |
| protected static class ColorPagerAdapter extends BasePagerAdapter<Integer> { |
| @Override |
| public Object instantiateItem(ViewGroup container, int position) { |
| final View view = new View(container.getContext()); |
| view.setBackgroundColor(mEntries.get(position).second); |
| configureInstantiatedItem(view, position); |
| |
| // Unlike ListView adapters, the ViewPager adapter is responsible |
| // for adding the view to the container. |
| container.addView(view); |
| |
| return new ViewHolder(view, position); |
| } |
| } |
| |
| protected static class TextPagerAdapter extends BasePagerAdapter<String> { |
| @Override |
| public Object instantiateItem(ViewGroup container, int position) { |
| final TextView view = new TextView(container.getContext()); |
| view.setText(mEntries.get(position).second); |
| configureInstantiatedItem(view, position); |
| |
| // Unlike ListView adapters, the ViewPager adapter is responsible |
| // for adding the view to the container. |
| container.addView(view); |
| |
| return new ViewHolder(view, position); |
| } |
| } |
| |
| public BaseViewPagerTest(Class<T> activityClass) { |
| super(activityClass); |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| final T activity = mActivityTestRule.getActivity(); |
| mViewPager = (ViewPager) activity.findViewById(R.id.pager); |
| |
| ColorPagerAdapter adapter = new ColorPagerAdapter(); |
| adapter.add("Red", Color.RED); |
| adapter.add("Green", Color.GREEN); |
| adapter.add("Blue", Color.BLUE); |
| onView(withId(R.id.pager)).perform(setAdapter(adapter), scrollToPage(0, false)); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| onView(withId(R.id.pager)).perform(setAdapter(null)); |
| } |
| |
| private void verifyPageSelections(boolean smoothScroll) { |
| assertEquals("Initial state", 0, mViewPager.getCurrentItem()); |
| |
| ViewPager.OnPageChangeListener mockPageChangeListener = |
| mock(ViewPager.OnPageChangeListener.class); |
| mViewPager.addOnPageChangeListener(mockPageChangeListener); |
| |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right", 1, mViewPager.getCurrentItem()); |
| verify(mockPageChangeListener, times(1)).onPageSelected(1); |
| |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right", 2, mViewPager.getCurrentItem()); |
| verify(mockPageChangeListener, times(1)).onPageSelected(2); |
| |
| // Try "scrolling" beyond the last page and test that we're still on the last page. |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right beyond last page", 2, mViewPager.getCurrentItem()); |
| // We're still on this page, so we shouldn't have been called again with index 2 |
| verify(mockPageChangeListener, times(1)).onPageSelected(2); |
| |
| onView(withId(R.id.pager)).perform(scrollLeft(smoothScroll)); |
| assertEquals("Scroll left", 1, mViewPager.getCurrentItem()); |
| // Verify that this is the second time we're called on index 1 |
| verify(mockPageChangeListener, times(2)).onPageSelected(1); |
| |
| onView(withId(R.id.pager)).perform(scrollLeft(smoothScroll)); |
| assertEquals("Scroll left", 0, mViewPager.getCurrentItem()); |
| // Verify that this is the first time we're called on index 0 |
| verify(mockPageChangeListener, times(1)).onPageSelected(0); |
| |
| // Try "scrolling" beyond the first page and test that we're still on the first page. |
| onView(withId(R.id.pager)).perform(scrollLeft(smoothScroll)); |
| assertEquals("Scroll left beyond first page", 0, mViewPager.getCurrentItem()); |
| // We're still on this page, so we shouldn't have been called again with index 0 |
| verify(mockPageChangeListener, times(1)).onPageSelected(0); |
| |
| // Unregister our listener |
| mViewPager.removeOnPageChangeListener(mockPageChangeListener); |
| |
| // Go from index 0 to index 2 |
| onView(withId(R.id.pager)).perform(scrollToPage(2, smoothScroll)); |
| assertEquals("Scroll to last page", 2, mViewPager.getCurrentItem()); |
| // Our listener is not registered anymore, so we shouldn't have been called with index 2 |
| verify(mockPageChangeListener, times(1)).onPageSelected(2); |
| |
| // And back to 0 |
| onView(withId(R.id.pager)).perform(scrollToPage(0, smoothScroll)); |
| assertEquals("Scroll to first page", 0, mViewPager.getCurrentItem()); |
| // Our listener is not registered anymore, so we shouldn't have been called with index 0 |
| verify(mockPageChangeListener, times(1)).onPageSelected(0); |
| |
| // Verify the overall sequence of calls to onPageSelected of our listener |
| ArgumentCaptor<Integer> pageSelectedCaptor = ArgumentCaptor.forClass(int.class); |
| verify(mockPageChangeListener, times(4)).onPageSelected(pageSelectedCaptor.capture()); |
| assertThat(pageSelectedCaptor.getAllValues(), TestUtilsMatchers.matches(1, 2, 1, 0)); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageSelectionsImmediate() { |
| verifyPageSelections(false); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageSelectionsSmooth() { |
| verifyPageSelections(true); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageSwipes() { |
| assertEquals("Initial state", 0, mViewPager.getCurrentItem()); |
| |
| ViewPager.OnPageChangeListener mockPageChangeListener = |
| mock(ViewPager.OnPageChangeListener.class); |
| mViewPager.addOnPageChangeListener(mockPageChangeListener); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeLeft())); |
| assertEquals("Swipe left", 1, mViewPager.getCurrentItem()); |
| verify(mockPageChangeListener, times(1)).onPageSelected(1); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeLeft())); |
| assertEquals("Swipe left", 2, mViewPager.getCurrentItem()); |
| verify(mockPageChangeListener, times(1)).onPageSelected(2); |
| |
| // Try swiping beyond the last page and test that we're still on the last page. |
| onView(withId(R.id.pager)).perform(wrap(swipeLeft())); |
| assertEquals("Swipe left beyond last page", 2, mViewPager.getCurrentItem()); |
| // We're still on this page, so we shouldn't have been called again with index 2 |
| verify(mockPageChangeListener, times(1)).onPageSelected(2); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeRight())); |
| assertEquals("Swipe right", 1, mViewPager.getCurrentItem()); |
| // Verify that this is the second time we're called on index 1 |
| verify(mockPageChangeListener, times(2)).onPageSelected(1); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeRight())); |
| assertEquals("Swipe right", 0, mViewPager.getCurrentItem()); |
| // Verify that this is the first time we're called on index 0 |
| verify(mockPageChangeListener, times(1)).onPageSelected(0); |
| |
| // Try swiping beyond the first page and test that we're still on the first page. |
| onView(withId(R.id.pager)).perform(wrap(swipeRight())); |
| assertEquals("Swipe right beyond first page", 0, mViewPager.getCurrentItem()); |
| // We're still on this page, so we shouldn't have been called again with index 0 |
| verify(mockPageChangeListener, times(1)).onPageSelected(0); |
| |
| mViewPager.removeOnPageChangeListener(mockPageChangeListener); |
| |
| // Verify the overall sequence of calls to onPageSelected of our listener |
| ArgumentCaptor<Integer> pageSelectedCaptor = ArgumentCaptor.forClass(int.class); |
| verify(mockPageChangeListener, times(4)).onPageSelected(pageSelectedCaptor.capture()); |
| assertThat(pageSelectedCaptor.getAllValues(), TestUtilsMatchers.matches(1, 2, 1, 0)); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageSwipesComposite() { |
| assertEquals("Initial state", 0, mViewPager.getCurrentItem()); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeLeft()), wrap(swipeLeft())); |
| assertEquals("Swipe twice left", 2, mViewPager.getCurrentItem()); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeLeft()), wrap(swipeRight())); |
| assertEquals("Swipe left beyond last page and then right", 1, mViewPager.getCurrentItem()); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeRight()), wrap(swipeRight())); |
| assertEquals("Swipe right and then right beyond first page", 0, |
| mViewPager.getCurrentItem()); |
| |
| onView(withId(R.id.pager)).perform(wrap(swipeRight()), wrap(swipeLeft())); |
| assertEquals("Swipe right beyond first page and then left", 1, mViewPager.getCurrentItem()); |
| } |
| |
| private void verifyPageContent(boolean smoothScroll) { |
| assertEquals("Initial state", 0, mViewPager.getCurrentItem()); |
| |
| // Verify the displayed content to match the initial adapter - with 3 pages and each |
| // one rendered as a View. |
| |
| // Page #0 should be displayed, page #1 should not be displayed and page #2 should not exist |
| // yet as it's outside of the offscreen window limit. |
| onView(withId(R.id.page_0)).check(matches(allOf( |
| isOfClass(View.class), |
| isDisplayed(), |
| backgroundColor(Color.RED)))); |
| onView(withId(R.id.page_1)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_2)).check(doesNotExist()); |
| |
| // Scroll one page to select page #1 |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right", 1, mViewPager.getCurrentItem()); |
| // Pages #0 / #2 should not be displayed, page #1 should be displayed. |
| onView(withId(R.id.page_0)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_1)).check(matches(allOf( |
| isOfClass(View.class), |
| isDisplayed(), |
| backgroundColor(Color.GREEN)))); |
| onView(withId(R.id.page_2)).check(matches(not(isDisplayed()))); |
| |
| // Scroll one more page to select page #2 |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right again", 2, mViewPager.getCurrentItem()); |
| // Page #0 should not exist as it's bumped to the outside of the offscreen window limit, |
| // page #1 should not be displayed, page #2 should be displayed. |
| onView(withId(R.id.page_0)).check(doesNotExist()); |
| onView(withId(R.id.page_1)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_2)).check(matches(allOf( |
| isOfClass(View.class), |
| isDisplayed(), |
| backgroundColor(Color.BLUE)))); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageContentImmediate() { |
| verifyPageContent(false); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageContentSmooth() { |
| verifyPageContent(true); |
| } |
| |
| private void verifyAdapterChange(boolean smoothScroll) { |
| // Verify that we have the expected initial adapter |
| PagerAdapter initialAdapter = mViewPager.getAdapter(); |
| assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass()); |
| assertEquals("Initial adapter page count", 3, initialAdapter.getCount()); |
| |
| // Create a new adapter |
| TextPagerAdapter newAdapter = new TextPagerAdapter(); |
| newAdapter.add("Title 0", "Body 0"); |
| newAdapter.add("Title 1", "Body 1"); |
| newAdapter.add("Title 2", "Body 2"); |
| newAdapter.add("Title 3", "Body 3"); |
| onView(withId(R.id.pager)).perform(setAdapter(newAdapter), scrollToPage(0, smoothScroll)); |
| |
| // Verify the displayed content to match the newly set adapter - with 4 pages and each |
| // one rendered as a TextView. |
| |
| // Page #0 should be displayed, page #1 should not be displayed and pages #2 / #3 should not |
| // exist yet as they're outside of the offscreen window limit. |
| onView(withId(R.id.page_0)).check(matches(allOf( |
| isOfClass(TextView.class), |
| isDisplayed(), |
| withText("Body 0")))); |
| onView(withId(R.id.page_1)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_2)).check(doesNotExist()); |
| onView(withId(R.id.page_3)).check(doesNotExist()); |
| |
| // Scroll one page to select page #1 |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right", 1, mViewPager.getCurrentItem()); |
| // Pages #0 / #2 should not be displayed, page #1 should be displayed, page #3 is still |
| // outside the offscreen limit. |
| onView(withId(R.id.page_0)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_1)).check(matches(allOf( |
| isOfClass(TextView.class), |
| isDisplayed(), |
| withText("Body 1")))); |
| onView(withId(R.id.page_2)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_3)).check(doesNotExist()); |
| |
| // Scroll one more page to select page #2 |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right again", 2, mViewPager.getCurrentItem()); |
| // Page #0 should not exist as it's bumped to the outside of the offscreen window limit, |
| // pages #1 / #3 should not be displayed, page #2 should be displayed. |
| onView(withId(R.id.page_0)).check(doesNotExist()); |
| onView(withId(R.id.page_1)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_2)).check(matches(allOf( |
| isOfClass(TextView.class), |
| isDisplayed(), |
| withText("Body 2")))); |
| onView(withId(R.id.page_3)).check(matches(not(isDisplayed()))); |
| |
| // Scroll one more page to select page #2 |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| assertEquals("Scroll right one more time", 3, mViewPager.getCurrentItem()); |
| // Pages #0 / #1 should not exist as they're bumped to the outside of the offscreen window |
| // limit, page #2 should not be displayed, page #3 should be displayed. |
| onView(withId(R.id.page_0)).check(doesNotExist()); |
| onView(withId(R.id.page_1)).check(doesNotExist()); |
| onView(withId(R.id.page_2)).check(matches(not(isDisplayed()))); |
| onView(withId(R.id.page_3)).check(matches(allOf( |
| isOfClass(TextView.class), |
| isDisplayed(), |
| withText("Body 3")))); |
| } |
| |
| @Test |
| @SmallTest |
| public void testAdapterChangeImmediate() { |
| verifyAdapterChange(false); |
| } |
| |
| @Test |
| @SmallTest |
| public void testAdapterChangeSmooth() { |
| verifyAdapterChange(true); |
| } |
| |
| private void verifyTitleStripLayout(String expectedStartTitle, String expectedSelectedTitle, |
| String expectedEndTitle, int selectedPageId) { |
| // Check that the title strip spans the whole width of the pager and is aligned to |
| // its top |
| onView(withId(R.id.titles)).check(isLeftAlignedWith(withId(R.id.pager))); |
| onView(withId(R.id.titles)).check(isRightAlignedWith(withId(R.id.pager))); |
| onView(withId(R.id.titles)).check(isTopAlignedWith(withId(R.id.pager))); |
| |
| // Check that the currently selected page spans the whole width of the pager and is below |
| // the title strip |
| onView(withId(selectedPageId)).check(isLeftAlignedWith(withId(R.id.pager))); |
| onView(withId(selectedPageId)).check(isRightAlignedWith(withId(R.id.pager))); |
| onView(withId(selectedPageId)).check(isBelow(withId(R.id.titles))); |
| onView(withId(selectedPageId)).check(isBottomAlignedWith(withId(R.id.pager))); |
| |
| boolean hasStartTitle = !TextUtils.isEmpty(expectedStartTitle); |
| boolean hasEndTitle = !TextUtils.isEmpty(expectedEndTitle); |
| |
| // Check that the title strip shows the expected number of children (tab titles) |
| int nonNullTitles = (hasStartTitle ? 1 : 0) + 1 + (hasEndTitle ? 1 : 0); |
| onView(withId(R.id.titles)).check(hasDisplayedChildren(nonNullTitles)); |
| |
| if (hasStartTitle) { |
| // Check that the title for the start page is displayed at the start edge of its parent |
| // (title strip) |
| onView(withId(R.id.titles)).check(matches(hasDescendant( |
| allOf(withText(expectedStartTitle), isDisplayed(), startAlignedToParent())))); |
| } |
| // Check that the title for the selected page is displayed centered in its parent |
| // (title strip) |
| onView(withId(R.id.titles)).check(matches(hasDescendant( |
| allOf(withText(expectedSelectedTitle), isDisplayed(), centerAlignedInParent())))); |
| if (hasEndTitle) { |
| // Check that the title for the end page is displayed at the end edge of its parent |
| // (title strip) |
| onView(withId(R.id.titles)).check(matches(hasDescendant( |
| allOf(withText(expectedEndTitle), isDisplayed(), endAlignedToParent())))); |
| } |
| } |
| |
| private void verifyPagerStrip(boolean smoothScroll) { |
| // Set an adapter with 5 pages |
| final ColorPagerAdapter adapter = new ColorPagerAdapter(); |
| adapter.add("Red", Color.RED); |
| adapter.add("Green", Color.GREEN); |
| adapter.add("Blue", Color.BLUE); |
| adapter.add("Yellow", Color.YELLOW); |
| adapter.add("Magenta", Color.MAGENTA); |
| onView(withId(R.id.pager)).perform(setAdapter(adapter), |
| scrollToPage(0, smoothScroll)); |
| |
| // Check that the pager has a title strip |
| onView(withId(R.id.pager)).check(matches(hasDescendant(withId(R.id.titles)))); |
| // Check that the title strip is displayed and is of the expected class |
| onView(withId(R.id.titles)).check(matches(allOf( |
| isDisplayed(), isOfClass(getStripClass())))); |
| |
| // The following block tests the overall layout of tab strip and main pager content |
| // (vertical stacking), the content of the tab strip (showing texts for the selected |
| // tab and the ones on its left / right) as well as the alignment of the content in the |
| // tab strip (selected in center, others on left and right). |
| |
| // Check the content and alignment of title strip for selected page #0 |
| verifyTitleStripLayout(null, "Red", "Green", R.id.page_0); |
| |
| // Scroll one page to select page #1 and check layout / content of title strip |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| verifyTitleStripLayout("Red", "Green", "Blue", R.id.page_1); |
| |
| // Scroll one page to select page #2 and check layout / content of title strip |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| verifyTitleStripLayout("Green", "Blue", "Yellow", R.id.page_2); |
| |
| // Scroll one page to select page #3 and check layout / content of title strip |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| verifyTitleStripLayout("Blue", "Yellow", "Magenta", R.id.page_3); |
| |
| // Scroll one page to select page #4 and check layout / content of title strip |
| onView(withId(R.id.pager)).perform(scrollRight(smoothScroll)); |
| verifyTitleStripLayout("Yellow", "Magenta", null, R.id.page_4); |
| |
| // Scroll back to page #0 |
| onView(withId(R.id.pager)).perform(scrollToPage(0, smoothScroll)); |
| |
| assertStripInteraction(smoothScroll); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPagerStripImmediate() { |
| verifyPagerStrip(false); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPagerStripSmooth() { |
| verifyPagerStrip(true); |
| } |
| |
| /** |
| * Returns the class of the pager strip. |
| */ |
| protected abstract Class getStripClass(); |
| |
| /** |
| * Checks assertions that are specific to the pager strip implementation (interactive or |
| * non interactive). |
| */ |
| protected abstract void assertStripInteraction(boolean smoothScroll); |
| |
| /** |
| * Helper method that performs the specified action on the <code>ViewPager</code> and then |
| * checks the sequence of calls to the page change listener based on the specified expected |
| * scroll state changes. |
| * |
| * If that expected list is empty, this method verifies that there were no calls to |
| * onPageScrollStateChanged when the action was performed. Otherwise it verifies that the actual |
| * sequence of calls to onPageScrollStateChanged matches the expected (specified) one. |
| */ |
| private void verifyScrollStateChange(ViewAction viewAction, int... expectedScrollStateChanges) { |
| ViewPager.OnPageChangeListener mockPageChangeListener = |
| mock(ViewPager.OnPageChangeListener.class); |
| mViewPager.addOnPageChangeListener(mockPageChangeListener); |
| |
| // Perform our action |
| onView(withId(R.id.pager)).perform(viewAction); |
| |
| int expectedScrollStateChangeCount = (expectedScrollStateChanges != null) ? |
| expectedScrollStateChanges.length : 0; |
| |
| if (expectedScrollStateChangeCount == 0) { |
| verify(mockPageChangeListener, never()).onPageScrollStateChanged(anyInt()); |
| } else { |
| ArgumentCaptor<Integer> pageScrollStateCaptor = ArgumentCaptor.forClass(int.class); |
| verify(mockPageChangeListener, times(expectedScrollStateChangeCount)). |
| onPageScrollStateChanged(pageScrollStateCaptor.capture()); |
| assertThat(pageScrollStateCaptor.getAllValues(), |
| TestUtilsMatchers.matches(expectedScrollStateChanges)); |
| } |
| |
| // Remove our mock listener to get back to clean state for the next test |
| mViewPager.removeOnPageChangeListener(mockPageChangeListener); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageScrollStateChangedImmediate() { |
| // Note that all the actions tested in this method are immediate (no scrolling) and |
| // as such we test that we do not get any calls to onPageScrollStateChanged in any of them |
| |
| // Select one page to the right |
| verifyScrollStateChange(scrollRight(false)); |
| // Select one more page to the right |
| verifyScrollStateChange(scrollRight(false)); |
| // Select one page to the left |
| verifyScrollStateChange(scrollLeft(false)); |
| // Select one more page to the left |
| verifyScrollStateChange(scrollLeft(false)); |
| // Select last page |
| verifyScrollStateChange(scrollToLast(false)); |
| // Select first page |
| verifyScrollStateChange(scrollToFirst(false)); |
| } |
| |
| @Test |
| @MediumTest |
| public void testPageScrollStateChangedSmooth() { |
| // Note that all the actions tested in this method use smooth scrolling and as such we test |
| // that we get the matching calls to onPageScrollStateChanged |
| final int[] expectedScrollStateChanges = new int[] { |
| ViewPager.SCROLL_STATE_SETTLING, ViewPager.SCROLL_STATE_IDLE |
| }; |
| |
| // Select one page to the right |
| verifyScrollStateChange(scrollRight(true), expectedScrollStateChanges); |
| // Select one more page to the right |
| verifyScrollStateChange(scrollRight(true), expectedScrollStateChanges); |
| // Select one page to the left |
| verifyScrollStateChange(scrollLeft(true), expectedScrollStateChanges); |
| // Select one more page to the left |
| verifyScrollStateChange(scrollLeft(true), expectedScrollStateChanges); |
| // Select last page |
| verifyScrollStateChange(scrollToLast(true), expectedScrollStateChanges); |
| // Select first page |
| verifyScrollStateChange(scrollToFirst(true), expectedScrollStateChanges); |
| } |
| |
| @Test |
| @MediumTest |
| public void testPageScrollStateChangedSwipe() { |
| // Note that all the actions tested in this method use swiping and as such we test |
| // that we get the matching calls to onPageScrollStateChanged |
| final int[] expectedScrollStateChanges = new int[] { ViewPager.SCROLL_STATE_DRAGGING, |
| ViewPager.SCROLL_STATE_SETTLING, ViewPager.SCROLL_STATE_IDLE }; |
| |
| // Swipe one page to the left |
| verifyScrollStateChange(wrap(swipeLeft()), expectedScrollStateChanges); |
| assertEquals("Swipe left", 1, mViewPager.getCurrentItem()); |
| |
| // Swipe one more page to the left |
| verifyScrollStateChange(wrap(swipeLeft()), expectedScrollStateChanges); |
| assertEquals("Swipe left", 2, mViewPager.getCurrentItem()); |
| |
| // Swipe one page to the right |
| verifyScrollStateChange(wrap(swipeRight()), expectedScrollStateChanges); |
| assertEquals("Swipe right", 1, mViewPager.getCurrentItem()); |
| |
| // Swipe one more page to the right |
| verifyScrollStateChange(wrap(swipeRight()), expectedScrollStateChanges); |
| assertEquals("Swipe right", 0, mViewPager.getCurrentItem()); |
| } |
| |
| /** |
| * Helper method to verify the internal consistency of values passed to |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} callback when we go from a page with |
| * lower index to a page with higher index. |
| * |
| * @param startPageIndex Index of the starting page. |
| * @param endPageIndex Index of the ending page. |
| * @param pageWidth Page width in pixels. |
| * @param positions List of "position" values passed to all |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} calls. |
| * @param positionOffsets List of "positionOffset" values passed to all |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} calls. |
| * @param positionOffsetPixels List of "positionOffsetPixel" values passed to all |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} calls. |
| */ |
| private void verifyScrollCallbacksToHigherPage(int startPageIndex, int endPageIndex, |
| int pageWidth, List<Integer> positions, List<Float> positionOffsets, |
| List<Integer> positionOffsetPixels) { |
| int callbackCount = positions.size(); |
| |
| // The last entry in all three lists must match the index of the end page |
| Assert.assertEquals("Position at last index", |
| endPageIndex, (int) positions.get(callbackCount - 1)); |
| Assert.assertEquals("Position offset at last index", |
| 0.0f, positionOffsets.get(callbackCount - 1), 0.0f); |
| Assert.assertEquals("Position offset pixel at last index", |
| 0, (int) positionOffsetPixels.get(callbackCount - 1)); |
| |
| // If this was our only callback, return. This can happen on immediate page change |
| // or on very slow devices. |
| if (callbackCount == 1) { |
| return; |
| } |
| |
| // If we have additional callbacks, verify that the values provided to our callback reflect |
| // a valid sequence of events going from startPageIndex to endPageIndex. |
| for (int i = 0; i < callbackCount - 1; i++) { |
| // Page position must be between start page and end page |
| int pagePositionCurr = positions.get(i); |
| if ((pagePositionCurr < startPageIndex) || (pagePositionCurr > endPageIndex)) { |
| Assert.fail("Position at #" + i + " is " + pagePositionCurr + |
| ", but should be between " + startPageIndex + " and " + endPageIndex); |
| } |
| |
| // Page position sequence cannot be decreasing |
| int pagePositionNext = positions.get(i + 1); |
| if (pagePositionCurr > pagePositionNext) { |
| Assert.fail("Position at #" + i + " is " + pagePositionCurr + |
| " and then decreases to " + pagePositionNext + " at #" + (i + 1)); |
| } |
| |
| // Position offset must be in [0..1) range (inclusive / exclusive) |
| float positionOffsetCurr = positionOffsets.get(i); |
| if ((positionOffsetCurr < 0.0f) || (positionOffsetCurr >= 1.0f)) { |
| Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr + |
| ", but should be in [0..1) range"); |
| } |
| |
| // Position pixel offset must be in [0..pageWidth) range (inclusive / exclusive) |
| int positionOffsetPixelCurr = positionOffsetPixels.get(i); |
| if ((positionOffsetPixelCurr < 0.0f) || (positionOffsetPixelCurr >= pageWidth)) { |
| Assert.fail("Position pixel offset at #" + i + " is " + positionOffsetCurr + |
| ", but should be in [0.." + pageWidth + ") range"); |
| } |
| |
| // Position pixel offset must match the position offset and page width within |
| // a one-pixel tolerance range |
| Assert.assertEquals("Position pixel offset at #" + i + " is " + |
| positionOffsetPixelCurr + ", but doesn't match position offset which is" + |
| positionOffsetCurr + " and page width which is " + pageWidth, |
| positionOffsetPixelCurr, positionOffsetCurr * pageWidth, 1.0f); |
| |
| // If we stay on the same page between this index and the next one, both position |
| // offset and position pixel offset must increase |
| if (pagePositionNext == pagePositionCurr) { |
| float positionOffsetNext = positionOffsets.get(i + 1); |
| // Note that since position offset sequence is float, we are checking for strict |
| // increasing |
| if (positionOffsetNext <= positionOffsetCurr) { |
| Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr + |
| " and at #" + (i + 1) + " is " + positionOffsetNext + |
| ". Since both are for page " + pagePositionCurr + |
| ", they cannot decrease"); |
| } |
| |
| int positionOffsetPixelNext = positionOffsetPixels.get(i + 1); |
| // Note that since position offset pixel sequence is the mapping of position offset |
| // into screen pixels, we can get two (or more) callbacks with strictly increasing |
| // position offsets that are converted into the same pixel value. This is why here |
| // we are checking for non-strict increasing |
| if (positionOffsetPixelNext < positionOffsetPixelCurr) { |
| Assert.fail("Position offset pixel at #" + i + " is " + |
| positionOffsetPixelCurr + " and at #" + (i + 1) + " is " + |
| positionOffsetPixelNext + ". Since both are for page " + |
| pagePositionCurr + ", they cannot decrease"); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Helper method to verify the internal consistency of values passed to |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} callback when we go from a page with |
| * higher index to a page with lower index. |
| * |
| * @param startPageIndex Index of the starting page. |
| * @param endPageIndex Index of the ending page. |
| * @param pageWidth Page width in pixels. |
| * @param positions List of "position" values passed to all |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} calls. |
| * @param positionOffsets List of "positionOffset" values passed to all |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} calls. |
| * @param positionOffsetPixels List of "positionOffsetPixel" values passed to all |
| * {@link ViewPager.OnPageChangeListener#onPageScrolled} calls. |
| */ |
| private void verifyScrollCallbacksToLowerPage(int startPageIndex, int endPageIndex, |
| int pageWidth, List<Integer> positions, List<Float> positionOffsets, |
| List<Integer> positionOffsetPixels) { |
| int callbackCount = positions.size(); |
| |
| // The last entry in all three lists must match the index of the end page |
| Assert.assertEquals("Position at last index", |
| endPageIndex, (int) positions.get(callbackCount - 1)); |
| Assert.assertEquals("Position offset at last index", |
| 0.0f, positionOffsets.get(callbackCount - 1), 0.0f); |
| Assert.assertEquals("Position offset pixel at last index", |
| 0, (int) positionOffsetPixels.get(callbackCount - 1)); |
| |
| // If this was our only callback, return. This can happen on immediate page change |
| // or on very slow devices. |
| if (callbackCount == 1) { |
| return; |
| } |
| |
| // If we have additional callbacks, verify that the values provided to our callback reflect |
| // a valid sequence of events going from startPageIndex to endPageIndex. |
| for (int i = 0; i < callbackCount - 1; i++) { |
| // Page position must be between start page and end page |
| int pagePositionCurr = positions.get(i); |
| if ((pagePositionCurr > startPageIndex) || (pagePositionCurr < endPageIndex)) { |
| Assert.fail("Position at #" + i + " is " + pagePositionCurr + |
| ", but should be between " + endPageIndex + " and " + startPageIndex); |
| } |
| |
| // Page position sequence cannot be increasing |
| int pagePositionNext = positions.get(i + 1); |
| if (pagePositionCurr < pagePositionNext) { |
| Assert.fail("Position at #" + i + " is " + pagePositionCurr + |
| " and then increases to " + pagePositionNext + " at #" + (i + 1)); |
| } |
| |
| // Position offset must be in [0..1) range (inclusive / exclusive) |
| float positionOffsetCurr = positionOffsets.get(i); |
| if ((positionOffsetCurr < 0.0f) || (positionOffsetCurr >= 1.0f)) { |
| Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr + |
| ", but should be in [0..1) range"); |
| } |
| |
| // Position pixel offset must be in [0..pageWidth) range (inclusive / exclusive) |
| int positionOffsetPixelCurr = positionOffsetPixels.get(i); |
| if ((positionOffsetPixelCurr < 0.0f) || (positionOffsetPixelCurr >= pageWidth)) { |
| Assert.fail("Position pixel offset at #" + i + " is " + positionOffsetCurr + |
| ", but should be in [0.." + pageWidth + ") range"); |
| } |
| |
| // Position pixel offset must match the position offset and page width within |
| // a one-pixel tolerance range |
| Assert.assertEquals("Position pixel offset at #" + i + " is " + |
| positionOffsetPixelCurr + ", but doesn't match position offset which is" + |
| positionOffsetCurr + " and page width which is " + pageWidth, |
| positionOffsetPixelCurr, positionOffsetCurr * pageWidth, 1.0f); |
| |
| // If we stay on the same page between this index and the next one, both position |
| // offset and position pixel offset must decrease |
| if (pagePositionNext == pagePositionCurr) { |
| float positionOffsetNext = positionOffsets.get(i + 1); |
| // Note that since position offset sequence is float, we are checking for strict |
| // decreasing |
| if (positionOffsetNext >= positionOffsetCurr) { |
| Assert.fail("Position offset at #" + i + " is " + positionOffsetCurr + |
| " and at #" + (i + 1) + " is " + positionOffsetNext + |
| ". Since both are for page " + pagePositionCurr + |
| ", they cannot increase"); |
| } |
| |
| int positionOffsetPixelNext = positionOffsetPixels.get(i + 1); |
| // Note that since position offset pixel sequence is the mapping of position offset |
| // into screen pixels, we can get two (or more) callbacks with strictly decreasing |
| // position offsets that are converted into the same pixel value. This is why here |
| // we are checking for non-strict decreasing |
| if (positionOffsetPixelNext > positionOffsetPixelCurr) { |
| Assert.fail("Position offset pixel at #" + i + " is " + |
| positionOffsetPixelCurr + " and at #" + (i + 1) + " is " + |
| positionOffsetPixelNext + ". Since both are for page " + |
| pagePositionCurr + ", they cannot increase"); |
| } |
| } |
| } |
| } |
| |
| private void verifyScrollCallbacksToHigherPage(ViewAction viewAction, |
| int expectedEndPageIndex) { |
| final int startPageIndex = mViewPager.getCurrentItem(); |
| |
| ViewPager.OnPageChangeListener mockPageChangeListener = |
| mock(ViewPager.OnPageChangeListener.class); |
| mViewPager.addOnPageChangeListener(mockPageChangeListener); |
| |
| // Perform our action |
| onView(withId(R.id.pager)).perform(viewAction); |
| |
| final int endPageIndex = mViewPager.getCurrentItem(); |
| Assert.assertEquals("Current item after action", expectedEndPageIndex, endPageIndex); |
| |
| ArgumentCaptor<Integer> positionCaptor = ArgumentCaptor.forClass(int.class); |
| ArgumentCaptor<Float> positionOffsetCaptor = ArgumentCaptor.forClass(float.class); |
| ArgumentCaptor<Integer> positionOffsetPixelsCaptor = ArgumentCaptor.forClass(int.class); |
| verify(mockPageChangeListener, atLeastOnce()).onPageScrolled(positionCaptor.capture(), |
| positionOffsetCaptor.capture(), positionOffsetPixelsCaptor.capture()); |
| |
| verifyScrollCallbacksToHigherPage(startPageIndex, endPageIndex, mViewPager.getWidth(), |
| positionCaptor.getAllValues(), positionOffsetCaptor.getAllValues(), |
| positionOffsetPixelsCaptor.getAllValues()); |
| |
| // Remove our mock listener to get back to clean state for the next test |
| mViewPager.removeOnPageChangeListener(mockPageChangeListener); |
| } |
| |
| private void verifyScrollCallbacksToLowerPage(ViewAction viewAction, |
| int expectedEndPageIndex) { |
| final int startPageIndex = mViewPager.getCurrentItem(); |
| |
| ViewPager.OnPageChangeListener mockPageChangeListener = |
| mock(ViewPager.OnPageChangeListener.class); |
| mViewPager.addOnPageChangeListener(mockPageChangeListener); |
| |
| // Perform our action |
| onView(withId(R.id.pager)).perform(viewAction); |
| |
| final int endPageIndex = mViewPager.getCurrentItem(); |
| Assert.assertEquals("Current item after action", expectedEndPageIndex, endPageIndex); |
| |
| ArgumentCaptor<Integer> positionCaptor = ArgumentCaptor.forClass(int.class); |
| ArgumentCaptor<Float> positionOffsetCaptor = ArgumentCaptor.forClass(float.class); |
| ArgumentCaptor<Integer> positionOffsetPixelsCaptor = ArgumentCaptor.forClass(int.class); |
| verify(mockPageChangeListener, atLeastOnce()).onPageScrolled(positionCaptor.capture(), |
| positionOffsetCaptor.capture(), positionOffsetPixelsCaptor.capture()); |
| |
| verifyScrollCallbacksToLowerPage(startPageIndex, endPageIndex, mViewPager.getWidth(), |
| positionCaptor.getAllValues(), positionOffsetCaptor.getAllValues(), |
| positionOffsetPixelsCaptor.getAllValues()); |
| |
| // Remove our mock listener to get back to clean state for the next test |
| mViewPager.removeOnPageChangeListener(mockPageChangeListener); |
| } |
| |
| @Test |
| @SmallTest |
| public void testPageScrollPositionChangesImmediate() { |
| // Scroll one page to the right |
| verifyScrollCallbacksToHigherPage(scrollRight(false), 1); |
| // Scroll one more page to the right |
| verifyScrollCallbacksToHigherPage(scrollRight(false), 2); |
| // Scroll one page to the left |
| verifyScrollCallbacksToLowerPage(scrollLeft(false), 1); |
| // Scroll one more page to the left |
| verifyScrollCallbacksToLowerPage(scrollLeft(false), 0); |
| |
| // Scroll to the last page |
| verifyScrollCallbacksToHigherPage(scrollToLast(false), 2); |
| // Scroll to the first page |
| verifyScrollCallbacksToLowerPage(scrollToFirst(false), 0); |
| } |
| |
| @Test |
| @MediumTest |
| public void testPageScrollPositionChangesSmooth() { |
| // Scroll one page to the right |
| verifyScrollCallbacksToHigherPage(scrollRight(true), 1); |
| // Scroll one more page to the right |
| verifyScrollCallbacksToHigherPage(scrollRight(true), 2); |
| // Scroll one page to the left |
| verifyScrollCallbacksToLowerPage(scrollLeft(true), 1); |
| // Scroll one more page to the left |
| verifyScrollCallbacksToLowerPage(scrollLeft(true), 0); |
| |
| // Scroll to the last page |
| verifyScrollCallbacksToHigherPage(scrollToLast(true), 2); |
| // Scroll to the first page |
| verifyScrollCallbacksToLowerPage(scrollToFirst(true), 0); |
| } |
| |
| @Test |
| @MediumTest |
| public void testPageScrollPositionChangesSwipe() { |
| // Swipe one page to the left |
| verifyScrollCallbacksToHigherPage(wrap(swipeLeft()), 1); |
| // Swipe one more page to the left |
| verifyScrollCallbacksToHigherPage(wrap(swipeLeft()), 2); |
| // Swipe one page to the right |
| verifyScrollCallbacksToLowerPage(wrap(swipeRight()), 1); |
| // Swipe one more page to the right |
| verifyScrollCallbacksToLowerPage(wrap(swipeRight()), 0); |
| } |
| } |