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