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.design.widget; 17 18import android.content.res.Resources; 19import android.graphics.Color; 20import android.support.annotation.DimenRes; 21import android.support.annotation.LayoutRes; 22import android.support.design.test.R; 23import android.support.design.testutils.TabLayoutActions; 24import android.support.design.testutils.TestUtilsActions; 25import android.support.design.testutils.TestUtilsMatchers; 26import android.support.design.testutils.ViewPagerActions; 27import android.support.v4.view.PagerAdapter; 28import android.support.v4.view.ViewPager; 29import android.test.suitebuilder.annotation.MediumTest; 30import android.test.suitebuilder.annotation.SmallTest; 31import android.util.Pair; 32import android.view.View; 33import android.view.ViewGroup; 34import android.widget.HorizontalScrollView; 35import android.widget.TextView; 36import org.hamcrest.Matcher; 37import org.junit.Before; 38import org.junit.Test; 39 40import java.util.ArrayList; 41 42import static android.support.design.testutils.TabLayoutActions.setupWithViewPager; 43import static android.support.design.testutils.ViewPagerActions.notifyAdapterContentChange; 44import static android.support.test.espresso.Espresso.onView; 45import static android.support.test.espresso.assertion.ViewAssertions.matches; 46import static android.support.test.espresso.matcher.ViewMatchers.*; 47import static org.hamcrest.Matchers.allOf; 48import static org.hamcrest.Matchers.not; 49import static org.junit.Assert.assertEquals; 50import static org.junit.Assert.assertNotEquals; 51 52public class TabLayoutWithViewPagerTest 53 extends BaseInstrumentationTestCase<TabLayoutWithViewPagerActivity> { 54 private TabLayout mTabLayout; 55 56 private ViewPager mViewPager; 57 58 private ColorPagerAdapter mDefaultPagerAdapter; 59 60 protected static class BasePagerAdapter<Q> extends PagerAdapter { 61 protected ArrayList<Pair<String, Q>> mEntries = new ArrayList<>(); 62 63 public void add(String title, Q content) { 64 mEntries.add(new Pair(title, content)); 65 } 66 67 @Override 68 public int getCount() { 69 return mEntries.size(); 70 } 71 72 protected void configureInstantiatedItem(View view, int position) { 73 switch (position) { 74 case 0: 75 view.setId(R.id.page_0); 76 break; 77 case 1: 78 view.setId(R.id.page_1); 79 break; 80 case 2: 81 view.setId(R.id.page_2); 82 break; 83 case 3: 84 view.setId(R.id.page_3); 85 break; 86 case 4: 87 view.setId(R.id.page_4); 88 break; 89 case 5: 90 view.setId(R.id.page_5); 91 break; 92 case 6: 93 view.setId(R.id.page_6); 94 break; 95 case 7: 96 view.setId(R.id.page_7); 97 break; 98 case 8: 99 view.setId(R.id.page_8); 100 break; 101 case 9: 102 view.setId(R.id.page_9); 103 break; 104 } 105 } 106 107 @Override 108 public void destroyItem(ViewGroup container, int position, Object object) { 109 // The adapter is also responsible for removing the view. 110 container.removeView(((ViewHolder) object).view); 111 } 112 113 @Override 114 public int getItemPosition(Object object) { 115 return ((ViewHolder) object).position; 116 } 117 118 @Override 119 public boolean isViewFromObject(View view, Object object) { 120 return ((ViewHolder) object).view == view; 121 } 122 123 @Override 124 public CharSequence getPageTitle(int position) { 125 return mEntries.get(position).first; 126 } 127 128 protected static class ViewHolder { 129 final View view; 130 final int position; 131 132 public ViewHolder(View view, int position) { 133 this.view = view; 134 this.position = position; 135 } 136 } 137 } 138 139 protected static class ColorPagerAdapter extends BasePagerAdapter<Integer> { 140 @Override 141 public Object instantiateItem(ViewGroup container, int position) { 142 final View view = new View(container.getContext()); 143 view.setBackgroundColor(mEntries.get(position).second); 144 configureInstantiatedItem(view, position); 145 146 // Unlike ListView adapters, the ViewPager adapter is responsible 147 // for adding the view to the container. 148 container.addView(view); 149 150 return new ViewHolder(view, position); 151 } 152 } 153 154 protected static class TextPagerAdapter extends BasePagerAdapter<String> { 155 @Override 156 public Object instantiateItem(ViewGroup container, int position) { 157 final TextView view = new TextView(container.getContext()); 158 view.setText(mEntries.get(position).second); 159 configureInstantiatedItem(view, position); 160 161 // Unlike ListView adapters, the ViewPager adapter is responsible 162 // for adding the view to the container. 163 container.addView(view); 164 165 return new ViewHolder(view, position); 166 } 167 } 168 169 public TabLayoutWithViewPagerTest() { 170 super(TabLayoutWithViewPagerActivity.class); 171 } 172 173 @Before 174 public void setUp() throws Exception { 175 final TabLayoutWithViewPagerActivity activity = mActivityTestRule.getActivity(); 176 mTabLayout = (TabLayout) activity.findViewById(R.id.tabs); 177 mViewPager = (ViewPager) activity.findViewById(R.id.tabs_viewpager); 178 179 mDefaultPagerAdapter = new ColorPagerAdapter(); 180 mDefaultPagerAdapter.add("Red", Color.RED); 181 mDefaultPagerAdapter.add("Green", Color.GREEN); 182 mDefaultPagerAdapter.add("Blue", Color.BLUE); 183 184 // Configure view pager 185 onView(withId(R.id.tabs_viewpager)).perform( 186 ViewPagerActions.setAdapter(mDefaultPagerAdapter), 187 ViewPagerActions.scrollToPage(0)); 188 } 189 190 private void setupTabLayoutWithViewPager() { 191 // And wire the tab layout to it 192 onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); 193 } 194 195 /** 196 * Verifies that selecting pages in <code>ViewPager</code> also updates the tab selection 197 * in the wired <code>TabLayout</code> 198 */ 199 private void verifyViewPagerSelection() { 200 int itemCount = mViewPager.getAdapter().getCount(); 201 202 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollToPage(0)); 203 assertEquals("Selected page", 0, mViewPager.getCurrentItem()); 204 assertEquals("Selected tab", 0, mTabLayout.getSelectedTabPosition()); 205 206 // Scroll tabs to the right 207 for (int i = 0; i < (itemCount - 1); i++) { 208 // Scroll one tab to the right 209 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollRight()); 210 final int expectedCurrentTabIndex = i + 1; 211 assertEquals("Scroll right #" + i, expectedCurrentTabIndex, 212 mViewPager.getCurrentItem()); 213 assertEquals("Selected tab after scrolling right #" + i, expectedCurrentTabIndex, 214 mTabLayout.getSelectedTabPosition()); 215 } 216 217 // Scroll tabs to the left 218 for (int i = 0; i < (itemCount - 1); i++) { 219 // Scroll one tab to the left 220 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollLeft()); 221 final int expectedCurrentTabIndex = itemCount - i - 2; 222 assertEquals("Scroll left #" + i, expectedCurrentTabIndex, mViewPager.getCurrentItem()); 223 assertEquals("Selected tab after scrolling left #" + i, expectedCurrentTabIndex, 224 mTabLayout.getSelectedTabPosition()); 225 } 226 } 227 228 /** 229 * Verifies that selecting pages in <code>ViewPager</code> also updates the tab selection 230 * in the wired <code>TabLayout</code> 231 */ 232 private void verifyTabLayoutSelection() { 233 int itemCount = mTabLayout.getTabCount(); 234 235 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollToPage(0)); 236 assertEquals("Selected tab", 0, mTabLayout.getSelectedTabPosition()); 237 assertEquals("Selected page", 0, mViewPager.getCurrentItem()); 238 239 // Select tabs "going" to the right. Note that the first loop iteration tests the 240 // scenario of "selecting" the first tab when it's already selected. 241 for (int i = 0; i < itemCount; i++) { 242 onView(withId(R.id.tabs)).perform(TabLayoutActions.selectTab(i)); 243 assertEquals("Selected tab after selecting #" + i, i, 244 mTabLayout.getSelectedTabPosition()); 245 assertEquals("Select tab #" + i, i, mViewPager.getCurrentItem()); 246 } 247 248 // Select tabs "going" to the left. Note that the first loop iteration tests the 249 // scenario of "selecting" the last tab when it's already selected. 250 for (int i = itemCount - 1; i >= 0; i--) { 251 onView(withId(R.id.tabs)).perform(TabLayoutActions.selectTab(i)); 252 assertEquals("Scroll left #" + i, i, mViewPager.getCurrentItem()); 253 assertEquals("Selected tab after scrolling left #" + i, i, 254 mTabLayout.getSelectedTabPosition()); 255 } 256 } 257 258 @Test 259 @SmallTest 260 public void testBasics() { 261 setupTabLayoutWithViewPager(); 262 263 final int itemCount = mViewPager.getAdapter().getCount(); 264 265 assertEquals("Matching item count", itemCount, mTabLayout.getTabCount()); 266 267 for (int i = 0; i < itemCount; i++) { 268 assertEquals("Tab #" +i, mViewPager.getAdapter().getPageTitle(i), 269 mTabLayout.getTabAt(i).getText()); 270 } 271 272 assertEquals("Selected tab", mViewPager.getCurrentItem(), 273 mTabLayout.getSelectedTabPosition()); 274 275 verifyViewPagerSelection(); 276 } 277 278 @Test 279 @SmallTest 280 public void testInteraction() { 281 setupTabLayoutWithViewPager(); 282 283 assertEquals("Default selected page", 0, mViewPager.getCurrentItem()); 284 assertEquals("Default selected tab", 0, mTabLayout.getSelectedTabPosition()); 285 286 verifyTabLayoutSelection(); 287 } 288 289 @Test 290 @SmallTest 291 public void testAdapterContentChange() { 292 setupTabLayoutWithViewPager(); 293 294 // Verify that we have the expected initial adapter 295 PagerAdapter initialAdapter = mViewPager.getAdapter(); 296 assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass()); 297 assertEquals("Initial adapter page count", 3, initialAdapter.getCount()); 298 299 // Add two more entries to our adapter 300 mDefaultPagerAdapter.add("Yellow", Color.YELLOW); 301 mDefaultPagerAdapter.add("Magenta", Color.MAGENTA); 302 final int newItemCount = mDefaultPagerAdapter.getCount(); 303 onView(withId(R.id.tabs_viewpager)).perform(notifyAdapterContentChange()); 304 305 // We have more comprehensive test coverage for changing the ViewPager adapter in v4/tests. 306 // Here we are focused on testing the continuous integration of TabLayout with the new 307 // content of ViewPager 308 309 assertEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); 310 311 for (int i = 0; i < newItemCount; i++) { 312 assertEquals("Tab #" +i, mViewPager.getAdapter().getPageTitle(i), 313 mTabLayout.getTabAt(i).getText()); 314 } 315 316 verifyViewPagerSelection(); 317 verifyTabLayoutSelection(); 318 } 319 320 @Test 321 @SmallTest 322 public void testAdapterContentChangeWithAutoRefreshDisabled() { 323 onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager, false)); 324 325 // Verify that we have the expected initial adapter 326 PagerAdapter initialAdapter = mViewPager.getAdapter(); 327 assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass()); 328 assertEquals("Initial adapter page count", 3, initialAdapter.getCount()); 329 330 // Add two more entries to our adapter 331 mDefaultPagerAdapter.add("Yellow", Color.YELLOW); 332 mDefaultPagerAdapter.add("Magenta", Color.MAGENTA); 333 final int newItemCount = mDefaultPagerAdapter.getCount(); 334 335 // Notify the adapter that it has changed 336 onView(withId(R.id.tabs_viewpager)).perform(notifyAdapterContentChange()); 337 338 // Assert that the TabLayout did not update and add the new items 339 assertNotEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); 340 } 341 342 @Test 343 @SmallTest 344 public void testBasicAutoRefreshDisabled() { 345 onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager, false)); 346 347 // Check that the TabLayout has the same number of items are the adapter 348 PagerAdapter initialAdapter = mViewPager.getAdapter(); 349 assertEquals("Initial adapter page count", initialAdapter.getCount(), 350 mTabLayout.getTabCount()); 351 352 // Add two more entries to our adapter 353 mDefaultPagerAdapter.add("Yellow", Color.YELLOW); 354 mDefaultPagerAdapter.add("Magenta", Color.MAGENTA); 355 final int newItemCount = mDefaultPagerAdapter.getCount(); 356 357 // Assert that the TabLayout did not update and add the new items 358 assertNotEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); 359 360 // Now setup again to update the tabs 361 onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager, false)); 362 363 // Assert that the TabLayout updated and added the new items 364 assertEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); 365 } 366 367 @Test 368 @SmallTest 369 public void testAdapterChange() { 370 setupTabLayoutWithViewPager(); 371 372 // Verify that we have the expected initial adapter 373 PagerAdapter initialAdapter = mViewPager.getAdapter(); 374 assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass()); 375 assertEquals("Initial adapter page count", 3, initialAdapter.getCount()); 376 377 // Create a new adapter 378 TextPagerAdapter newAdapter = new TextPagerAdapter(); 379 final int newItemCount = 6; 380 for (int i = 0; i < newItemCount; i++) { 381 newAdapter.add("Title " + i, "Body " + i); 382 } 383 // And set it on the ViewPager 384 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.setAdapter(newAdapter), 385 ViewPagerActions.scrollToPage(0)); 386 387 // As TabLayout doesn't track adapter changes, we need to re-wire the new adapter 388 onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); 389 390 // We have more comprehensive test coverage for changing the ViewPager adapter in v4/tests. 391 // Here we are focused on testing the integration of TabLayout with the new 392 // content of ViewPager 393 394 assertEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); 395 396 for (int i = 0; i < newItemCount; i++) { 397 assertEquals("Tab #" +i, mViewPager.getAdapter().getPageTitle(i), 398 mTabLayout.getTabAt(i).getText()); 399 } 400 401 verifyViewPagerSelection(); 402 verifyTabLayoutSelection(); 403 } 404 405 @Test 406 @MediumTest 407 public void testFixedTabMode() { 408 // Create a new adapter (with no content) 409 final TextPagerAdapter newAdapter = new TextPagerAdapter(); 410 // And set it on the ViewPager 411 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.setAdapter(newAdapter)); 412 // As TabLayout doesn't track adapter changes, we need to re-wire the new adapter 413 onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); 414 415 // Set fixed mode on the TabLayout 416 onView(withId(R.id.tabs)).perform(TabLayoutActions.setTabMode(TabLayout.MODE_FIXED)); 417 assertEquals("Fixed tab mode", TabLayout.MODE_FIXED, mTabLayout.getTabMode()); 418 419 // Add a bunch of tabs and verify that all of them are visible on the screen 420 for (int i = 0; i < 8; i++) { 421 newAdapter.add("Title " + i, "Body " + i); 422 onView(withId(R.id.tabs_viewpager)).perform( 423 notifyAdapterContentChange()); 424 425 int expectedTabCount = i + 1; 426 assertEquals("Tab count after adding #" + i, expectedTabCount, 427 mTabLayout.getTabCount()); 428 assertEquals("Page count after adding #" + i, expectedTabCount, 429 mViewPager.getAdapter().getCount()); 430 431 verifyViewPagerSelection(); 432 verifyTabLayoutSelection(); 433 434 // Check that all tabs are fully visible (the content may or may not be elided) 435 for (int j = 0; j < expectedTabCount; j++) { 436 onView(allOf(isDescendantOfA(withId(R.id.tabs)), withText("Title " + j))). 437 check(matches(isCompletelyDisplayed())); 438 } 439 } 440 } 441 442 /** 443 * Helper method to verify support for min and max tab width on TabLayout in scrollable mode. 444 * It replaces the TabLayout based on the passed layout resource ID and then adds a bunch of 445 * tab titles to the wired ViewPager with progressively longer texts. After each tab is added 446 * this method then checks that all tab views respect the minimum and maximum tab width set 447 * on TabLayout. 448 * 449 * @param tabLayoutResId Layout resource for the TabLayout to be wired to the ViewPager. 450 * @param tabMinWidthResId If non zero, points to the dimension resource to use for tab min 451 * width check. 452 * @param tabMaxWidthResId If non zero, points to the dimension resource to use for tab max 453 * width check. 454 */ 455 private void verifyMinMaxTabWidth(@LayoutRes int tabLayoutResId, @DimenRes int tabMinWidthResId, 456 @DimenRes int tabMaxWidthResId) { 457 setupTabLayoutWithViewPager(); 458 459 assertEquals("Scrollable tab mode", TabLayout.MODE_SCROLLABLE, mTabLayout.getTabMode()); 460 461 final Resources res = mActivityTestRule.getActivity().getResources(); 462 final int minTabWidth = (tabMinWidthResId == 0) ? -1 : 463 res.getDimensionPixelSize(tabMinWidthResId); 464 final int maxTabWidth = (tabMaxWidthResId == 0) ? -1 : 465 res.getDimensionPixelSize(tabMaxWidthResId); 466 467 // Create a new adapter (with no content) 468 final TextPagerAdapter newAdapter = new TextPagerAdapter(); 469 // And set it on the ViewPager 470 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.setAdapter(newAdapter)); 471 472 // Replace the default TabLayout with the passed one 473 onView(withId(R.id.container)).perform(TestUtilsActions.replaceTabLayout(tabLayoutResId)); 474 475 // Now that we have a new TabLayout, wire it to the new content of our ViewPager 476 onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); 477 478 // Since TabLayout doesn't expose a getter for fetching the configured max tab width, 479 // start adding a variety of tabs with progressively longer tab titles and test that 480 // no tab is wider than the configured max width. Before we start that test, 481 // verify that we're in the scrollable mode so that each tab title gets as much width 482 // as needed to display its text. 483 assertEquals("Scrollable tab mode", TabLayout.MODE_SCROLLABLE, mTabLayout.getTabMode()); 484 485 final StringBuilder tabTitleBuilder = new StringBuilder(); 486 for (int i = 0; i < 40; i++) { 487 final char titleComponent = (char) ('A' + i); 488 for (int j = 0; j <= (i + 1); j++) { 489 tabTitleBuilder.append(titleComponent); 490 } 491 final String tabTitle = tabTitleBuilder.toString(); 492 newAdapter.add(tabTitle, "Body " + i); 493 onView(withId(R.id.tabs_viewpager)).perform( 494 notifyAdapterContentChange()); 495 496 int expectedTabCount = i + 1; 497 // Check that all tabs are at least as wide as min width *and* at most as wide as max 498 // width specified in the XML for the newly loaded TabLayout 499 for (int j = 0; j < expectedTabCount; j++) { 500 // Find the view that is our tab title. It should be: 501 // 1. Descendant of our TabLayout 502 // 2. But not a direct child of the horizontal scroller 503 // 3. With just-added title text 504 // These conditions make sure that we're selecting the "top-level" tab view 505 // instead of the inner (and narrower) TextView 506 Matcher<View> tabMatcher = allOf( 507 isDescendantOfA(withId(R.id.tabs)), 508 not(withParent(isAssignableFrom(HorizontalScrollView.class))), 509 hasDescendant(withText(tabTitle))); 510 if (minTabWidth >= 0) { 511 onView(tabMatcher).check(matches( 512 TestUtilsMatchers.isNotNarrowerThan(minTabWidth))); 513 } 514 if (maxTabWidth >= 0) { 515 onView(tabMatcher).check(matches( 516 TestUtilsMatchers.isNotWiderThan(maxTabWidth))); 517 } 518 } 519 520 // Reset the title builder for the next tab 521 tabTitleBuilder.setLength(0); 522 tabTitleBuilder.trimToSize(); 523 } 524 525 } 526 527 @Test 528 @MediumTest 529 public void testMinTabWidth() { 530 verifyMinMaxTabWidth(R.layout.tab_layout_bound_min, R.dimen.tab_width_limit_medium, 0); 531 } 532 533 @Test 534 @MediumTest 535 public void testMaxTabWidth() { 536 verifyMinMaxTabWidth(R.layout.tab_layout_bound_max, 0, R.dimen.tab_width_limit_medium); 537 } 538 539 @Test 540 @MediumTest 541 public void testMinMaxTabWidth() { 542 verifyMinMaxTabWidth(R.layout.tab_layout_bound_minmax, R.dimen.tab_width_limit_small, 543 R.dimen.tab_width_limit_large); 544 } 545 546 @Test 547 @SmallTest 548 public void testSetupAfterViewPagerScrolled() { 549 // Scroll to the last item 550 final int selected = mViewPager.getAdapter().getCount() - 1; 551 onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollToPage(selected)); 552 553 // Now setup the TabLayout with the ViewPager 554 setupTabLayoutWithViewPager(); 555 556 assertEquals("Selected page", selected, mViewPager.getCurrentItem()); 557 assertEquals("Selected tab", selected, mTabLayout.getSelectedTabPosition()); 558 } 559} 560