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