/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.design.widget; import android.content.res.Resources; import android.graphics.Color; import android.support.annotation.DimenRes; import android.support.annotation.LayoutRes; import android.support.design.test.R; import android.support.design.testutils.TabLayoutActions; import android.support.design.testutils.TestUtilsActions; import android.support.design.testutils.TestUtilsMatchers; import android.support.design.testutils.ViewPagerActions; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; import android.util.Pair; import android.view.View; import android.view.ViewGroup; import android.widget.HorizontalScrollView; import android.widget.TextView; import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; import java.util.ArrayList; import static android.support.design.testutils.TabLayoutActions.setupWithViewPager; import static android.support.design.testutils.ViewPagerActions.notifyAdapterContentChange; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.*; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; public class TabLayoutWithViewPagerTest extends BaseInstrumentationTestCase { private TabLayout mTabLayout; private ViewPager mViewPager; private ColorPagerAdapter mDefaultPagerAdapter; protected static class BasePagerAdapter extends PagerAdapter { protected ArrayList> mEntries = new ArrayList<>(); public void add(String title, Q content) { mEntries.add(new Pair(title, content)); } @Override public int getCount() { return mEntries.size(); } protected void configureInstantiatedItem(View view, int position) { switch (position) { case 0: view.setId(R.id.page_0); break; case 1: view.setId(R.id.page_1); break; case 2: view.setId(R.id.page_2); break; case 3: view.setId(R.id.page_3); break; case 4: view.setId(R.id.page_4); break; case 5: view.setId(R.id.page_5); break; case 6: view.setId(R.id.page_6); break; case 7: view.setId(R.id.page_7); break; case 8: view.setId(R.id.page_8); break; case 9: view.setId(R.id.page_9); break; } } @Override public void destroyItem(ViewGroup container, int position, Object object) { // The adapter is also responsible for removing the view. container.removeView(((ViewHolder) object).view); } @Override public int getItemPosition(Object object) { return ((ViewHolder) object).position; } @Override public boolean isViewFromObject(View view, Object object) { return ((ViewHolder) object).view == view; } @Override public CharSequence getPageTitle(int position) { return mEntries.get(position).first; } protected static class ViewHolder { final View view; final int position; public ViewHolder(View view, int position) { this.view = view; this.position = position; } } } protected static class ColorPagerAdapter extends BasePagerAdapter { @Override public Object instantiateItem(ViewGroup container, int position) { final View view = new View(container.getContext()); view.setBackgroundColor(mEntries.get(position).second); configureInstantiatedItem(view, position); // Unlike ListView adapters, the ViewPager adapter is responsible // for adding the view to the container. container.addView(view); return new ViewHolder(view, position); } } protected static class TextPagerAdapter extends BasePagerAdapter { @Override public Object instantiateItem(ViewGroup container, int position) { final TextView view = new TextView(container.getContext()); view.setText(mEntries.get(position).second); configureInstantiatedItem(view, position); // Unlike ListView adapters, the ViewPager adapter is responsible // for adding the view to the container. container.addView(view); return new ViewHolder(view, position); } } public TabLayoutWithViewPagerTest() { super(TabLayoutWithViewPagerActivity.class); } @Before public void setUp() throws Exception { final TabLayoutWithViewPagerActivity activity = mActivityTestRule.getActivity(); mTabLayout = (TabLayout) activity.findViewById(R.id.tabs); mViewPager = (ViewPager) activity.findViewById(R.id.tabs_viewpager); mDefaultPagerAdapter = new ColorPagerAdapter(); mDefaultPagerAdapter.add("Red", Color.RED); mDefaultPagerAdapter.add("Green", Color.GREEN); mDefaultPagerAdapter.add("Blue", Color.BLUE); // Configure view pager onView(withId(R.id.tabs_viewpager)).perform( ViewPagerActions.setAdapter(mDefaultPagerAdapter), ViewPagerActions.scrollToPage(0)); } private void setupTabLayoutWithViewPager() { // And wire the tab layout to it onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); } /** * Verifies that selecting pages in ViewPager also updates the tab selection * in the wired TabLayout */ private void verifyViewPagerSelection() { int itemCount = mViewPager.getAdapter().getCount(); onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollToPage(0)); assertEquals("Selected page", 0, mViewPager.getCurrentItem()); assertEquals("Selected tab", 0, mTabLayout.getSelectedTabPosition()); // Scroll tabs to the right for (int i = 0; i < (itemCount - 1); i++) { // Scroll one tab to the right onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollRight()); final int expectedCurrentTabIndex = i + 1; assertEquals("Scroll right #" + i, expectedCurrentTabIndex, mViewPager.getCurrentItem()); assertEquals("Selected tab after scrolling right #" + i, expectedCurrentTabIndex, mTabLayout.getSelectedTabPosition()); } // Scroll tabs to the left for (int i = 0; i < (itemCount - 1); i++) { // Scroll one tab to the left onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollLeft()); final int expectedCurrentTabIndex = itemCount - i - 2; assertEquals("Scroll left #" + i, expectedCurrentTabIndex, mViewPager.getCurrentItem()); assertEquals("Selected tab after scrolling left #" + i, expectedCurrentTabIndex, mTabLayout.getSelectedTabPosition()); } } /** * Verifies that selecting pages in ViewPager also updates the tab selection * in the wired TabLayout */ private void verifyTabLayoutSelection() { int itemCount = mTabLayout.getTabCount(); onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollToPage(0)); assertEquals("Selected tab", 0, mTabLayout.getSelectedTabPosition()); assertEquals("Selected page", 0, mViewPager.getCurrentItem()); // Select tabs "going" to the right. Note that the first loop iteration tests the // scenario of "selecting" the first tab when it's already selected. for (int i = 0; i < itemCount; i++) { onView(withId(R.id.tabs)).perform(TabLayoutActions.selectTab(i)); assertEquals("Selected tab after selecting #" + i, i, mTabLayout.getSelectedTabPosition()); assertEquals("Select tab #" + i, i, mViewPager.getCurrentItem()); } // Select tabs "going" to the left. Note that the first loop iteration tests the // scenario of "selecting" the last tab when it's already selected. for (int i = itemCount - 1; i >= 0; i--) { onView(withId(R.id.tabs)).perform(TabLayoutActions.selectTab(i)); assertEquals("Scroll left #" + i, i, mViewPager.getCurrentItem()); assertEquals("Selected tab after scrolling left #" + i, i, mTabLayout.getSelectedTabPosition()); } } @Test @SmallTest public void testBasics() { setupTabLayoutWithViewPager(); final int itemCount = mViewPager.getAdapter().getCount(); assertEquals("Matching item count", itemCount, mTabLayout.getTabCount()); for (int i = 0; i < itemCount; i++) { assertEquals("Tab #" +i, mViewPager.getAdapter().getPageTitle(i), mTabLayout.getTabAt(i).getText()); } assertEquals("Selected tab", mViewPager.getCurrentItem(), mTabLayout.getSelectedTabPosition()); verifyViewPagerSelection(); } @Test @SmallTest public void testInteraction() { setupTabLayoutWithViewPager(); assertEquals("Default selected page", 0, mViewPager.getCurrentItem()); assertEquals("Default selected tab", 0, mTabLayout.getSelectedTabPosition()); verifyTabLayoutSelection(); } @Test @SmallTest public void testAdapterContentChange() { setupTabLayoutWithViewPager(); // Verify that we have the expected initial adapter PagerAdapter initialAdapter = mViewPager.getAdapter(); assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass()); assertEquals("Initial adapter page count", 3, initialAdapter.getCount()); // Add two more entries to our adapter mDefaultPagerAdapter.add("Yellow", Color.YELLOW); mDefaultPagerAdapter.add("Magenta", Color.MAGENTA); final int newItemCount = mDefaultPagerAdapter.getCount(); onView(withId(R.id.tabs_viewpager)).perform(notifyAdapterContentChange()); // We have more comprehensive test coverage for changing the ViewPager adapter in v4/tests. // Here we are focused on testing the continuous integration of TabLayout with the new // content of ViewPager assertEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); for (int i = 0; i < newItemCount; i++) { assertEquals("Tab #" +i, mViewPager.getAdapter().getPageTitle(i), mTabLayout.getTabAt(i).getText()); } verifyViewPagerSelection(); verifyTabLayoutSelection(); } @Test @SmallTest public void testAdapterContentChangeWithAutoRefreshDisabled() { onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager, false)); // Verify that we have the expected initial adapter PagerAdapter initialAdapter = mViewPager.getAdapter(); assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass()); assertEquals("Initial adapter page count", 3, initialAdapter.getCount()); // Add two more entries to our adapter mDefaultPagerAdapter.add("Yellow", Color.YELLOW); mDefaultPagerAdapter.add("Magenta", Color.MAGENTA); final int newItemCount = mDefaultPagerAdapter.getCount(); // Notify the adapter that it has changed onView(withId(R.id.tabs_viewpager)).perform(notifyAdapterContentChange()); // Assert that the TabLayout did not update and add the new items assertNotEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); } @Test @SmallTest public void testBasicAutoRefreshDisabled() { onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager, false)); // Check that the TabLayout has the same number of items are the adapter PagerAdapter initialAdapter = mViewPager.getAdapter(); assertEquals("Initial adapter page count", initialAdapter.getCount(), mTabLayout.getTabCount()); // Add two more entries to our adapter mDefaultPagerAdapter.add("Yellow", Color.YELLOW); mDefaultPagerAdapter.add("Magenta", Color.MAGENTA); final int newItemCount = mDefaultPagerAdapter.getCount(); // Assert that the TabLayout did not update and add the new items assertNotEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); // Now setup again to update the tabs onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager, false)); // Assert that the TabLayout updated and added the new items assertEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); } @Test @SmallTest public void testAdapterChange() { setupTabLayoutWithViewPager(); // Verify that we have the expected initial adapter PagerAdapter initialAdapter = mViewPager.getAdapter(); assertEquals("Initial adapter class", ColorPagerAdapter.class, initialAdapter.getClass()); assertEquals("Initial adapter page count", 3, initialAdapter.getCount()); // Create a new adapter TextPagerAdapter newAdapter = new TextPagerAdapter(); final int newItemCount = 6; for (int i = 0; i < newItemCount; i++) { newAdapter.add("Title " + i, "Body " + i); } // And set it on the ViewPager onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.setAdapter(newAdapter), ViewPagerActions.scrollToPage(0)); // As TabLayout doesn't track adapter changes, we need to re-wire the new adapter onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); // We have more comprehensive test coverage for changing the ViewPager adapter in v4/tests. // Here we are focused on testing the integration of TabLayout with the new // content of ViewPager assertEquals("Matching item count", newItemCount, mTabLayout.getTabCount()); for (int i = 0; i < newItemCount; i++) { assertEquals("Tab #" +i, mViewPager.getAdapter().getPageTitle(i), mTabLayout.getTabAt(i).getText()); } verifyViewPagerSelection(); verifyTabLayoutSelection(); } @Test @MediumTest public void testFixedTabMode() { // Create a new adapter (with no content) final TextPagerAdapter newAdapter = new TextPagerAdapter(); // And set it on the ViewPager onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.setAdapter(newAdapter)); // As TabLayout doesn't track adapter changes, we need to re-wire the new adapter onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); // Set fixed mode on the TabLayout onView(withId(R.id.tabs)).perform(TabLayoutActions.setTabMode(TabLayout.MODE_FIXED)); assertEquals("Fixed tab mode", TabLayout.MODE_FIXED, mTabLayout.getTabMode()); // Add a bunch of tabs and verify that all of them are visible on the screen for (int i = 0; i < 8; i++) { newAdapter.add("Title " + i, "Body " + i); onView(withId(R.id.tabs_viewpager)).perform( notifyAdapterContentChange()); int expectedTabCount = i + 1; assertEquals("Tab count after adding #" + i, expectedTabCount, mTabLayout.getTabCount()); assertEquals("Page count after adding #" + i, expectedTabCount, mViewPager.getAdapter().getCount()); verifyViewPagerSelection(); verifyTabLayoutSelection(); // Check that all tabs are fully visible (the content may or may not be elided) for (int j = 0; j < expectedTabCount; j++) { onView(allOf(isDescendantOfA(withId(R.id.tabs)), withText("Title " + j))). check(matches(isCompletelyDisplayed())); } } } /** * Helper method to verify support for min and max tab width on TabLayout in scrollable mode. * It replaces the TabLayout based on the passed layout resource ID and then adds a bunch of * tab titles to the wired ViewPager with progressively longer texts. After each tab is added * this method then checks that all tab views respect the minimum and maximum tab width set * on TabLayout. * * @param tabLayoutResId Layout resource for the TabLayout to be wired to the ViewPager. * @param tabMinWidthResId If non zero, points to the dimension resource to use for tab min * width check. * @param tabMaxWidthResId If non zero, points to the dimension resource to use for tab max * width check. */ private void verifyMinMaxTabWidth(@LayoutRes int tabLayoutResId, @DimenRes int tabMinWidthResId, @DimenRes int tabMaxWidthResId) { setupTabLayoutWithViewPager(); assertEquals("Scrollable tab mode", TabLayout.MODE_SCROLLABLE, mTabLayout.getTabMode()); final Resources res = mActivityTestRule.getActivity().getResources(); final int minTabWidth = (tabMinWidthResId == 0) ? -1 : res.getDimensionPixelSize(tabMinWidthResId); final int maxTabWidth = (tabMaxWidthResId == 0) ? -1 : res.getDimensionPixelSize(tabMaxWidthResId); // Create a new adapter (with no content) final TextPagerAdapter newAdapter = new TextPagerAdapter(); // And set it on the ViewPager onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.setAdapter(newAdapter)); // Replace the default TabLayout with the passed one onView(withId(R.id.container)).perform(TestUtilsActions.replaceTabLayout(tabLayoutResId)); // Now that we have a new TabLayout, wire it to the new content of our ViewPager onView(withId(R.id.tabs)).perform(setupWithViewPager(mViewPager)); // Since TabLayout doesn't expose a getter for fetching the configured max tab width, // start adding a variety of tabs with progressively longer tab titles and test that // no tab is wider than the configured max width. Before we start that test, // verify that we're in the scrollable mode so that each tab title gets as much width // as needed to display its text. assertEquals("Scrollable tab mode", TabLayout.MODE_SCROLLABLE, mTabLayout.getTabMode()); final StringBuilder tabTitleBuilder = new StringBuilder(); for (int i = 0; i < 40; i++) { final char titleComponent = (char) ('A' + i); for (int j = 0; j <= (i + 1); j++) { tabTitleBuilder.append(titleComponent); } final String tabTitle = tabTitleBuilder.toString(); newAdapter.add(tabTitle, "Body " + i); onView(withId(R.id.tabs_viewpager)).perform( notifyAdapterContentChange()); int expectedTabCount = i + 1; // Check that all tabs are at least as wide as min width *and* at most as wide as max // width specified in the XML for the newly loaded TabLayout for (int j = 0; j < expectedTabCount; j++) { // Find the view that is our tab title. It should be: // 1. Descendant of our TabLayout // 2. But not a direct child of the horizontal scroller // 3. With just-added title text // These conditions make sure that we're selecting the "top-level" tab view // instead of the inner (and narrower) TextView Matcher tabMatcher = allOf( isDescendantOfA(withId(R.id.tabs)), not(withParent(isAssignableFrom(HorizontalScrollView.class))), hasDescendant(withText(tabTitle))); if (minTabWidth >= 0) { onView(tabMatcher).check(matches( TestUtilsMatchers.isNotNarrowerThan(minTabWidth))); } if (maxTabWidth >= 0) { onView(tabMatcher).check(matches( TestUtilsMatchers.isNotWiderThan(maxTabWidth))); } } // Reset the title builder for the next tab tabTitleBuilder.setLength(0); tabTitleBuilder.trimToSize(); } } @Test @MediumTest public void testMinTabWidth() { verifyMinMaxTabWidth(R.layout.tab_layout_bound_min, R.dimen.tab_width_limit_medium, 0); } @Test @MediumTest public void testMaxTabWidth() { verifyMinMaxTabWidth(R.layout.tab_layout_bound_max, 0, R.dimen.tab_width_limit_medium); } @Test @MediumTest public void testMinMaxTabWidth() { verifyMinMaxTabWidth(R.layout.tab_layout_bound_minmax, R.dimen.tab_width_limit_small, R.dimen.tab_width_limit_large); } @Test @SmallTest public void testSetupAfterViewPagerScrolled() { // Scroll to the last item final int selected = mViewPager.getAdapter().getCount() - 1; onView(withId(R.id.tabs_viewpager)).perform(ViewPagerActions.scrollToPage(selected)); // Now setup the TabLayout with the ViewPager setupTabLayoutWithViewPager(); assertEquals("Selected page", selected, mViewPager.getCurrentItem()); assertEquals("Selected tab", selected, mTabLayout.getSelectedTabPosition()); } }