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