/* * Copyright (C) 2016 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 static android.support.design.testutils.TabLayoutActions.selectTab; import static android.support.design.testutils.TabLayoutActions.setScrollPosition; import static android.support.design.testutils.TestUtilsActions.setLayoutDirection; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import android.support.design.test.R; import android.support.test.annotation.UiThreadTest; import android.support.test.espresso.Espresso; import android.support.test.espresso.IdlingResource; import android.support.test.espresso.NoMatchingViewException; import android.support.test.espresso.ViewAssertion; import android.support.test.filters.SmallTest; import android.support.v4.view.ViewCompat; import android.support.v7.app.AppCompatActivity; import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import org.junit.Test; import java.util.concurrent.atomic.AtomicInteger; @SmallTest public class TabLayoutTest extends BaseInstrumentationTestCase<AppCompatActivity> { public TabLayoutTest() { super(AppCompatActivity.class); } @Test @UiThreadTest public void testInflateTabLayoutWithTabItems() { final LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity()); final TabLayout tabLayout = (TabLayout) inflater.inflate(R.layout.design_tabs_items, null); assertEquals(3, tabLayout.getTabCount()); // Tab 0 has text, but no icon or custom view TabLayout.Tab tab = tabLayout.getTabAt(0); assertEquals(mActivityTestRule.getActivity().getString(R.string.tab_layout_text), tab.getText()); assertNull(tab.getIcon()); assertNull(tab.getCustomView()); // Tab 1 has an icon, but no text or custom view tab = tabLayout.getTabAt(1); assertNull(tab.getText()); assertNotNull(tab.getIcon()); assertNull(tab.getCustomView()); // Tab 2 has a custom view, but no text or icon tab = tabLayout.getTabAt(2); assertNull(tab.getText()); assertNull(tab.getIcon()); assertNotNull(tab.getCustomView()); assertEquals(R.id.my_custom_tab, tab.getCustomView().getId()); } @Test @UiThreadTest public void testInflateTabLayoutWithNonTabItem() throws Throwable { try { final LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity()); inflater.inflate(R.layout.design_tabs_with_non_tabitems, null); } catch (Throwable throwable) { assertTrue(throwable instanceof InflateException || throwable instanceof IllegalArgumentException); } } @Test @UiThreadTest public void testTabWithCustomLayoutSelection1() { final TabLayout.OnTabSelectedListener mockListener = mock(TabLayout.OnTabSelectedListener.class); final LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity()); final TabLayout tabLayout = (TabLayout) inflater.inflate(R.layout.design_tabs, null); tabLayout.addOnTabSelectedListener(mockListener); final TabLayout.Tab tab = tabLayout.newTab(); tab.setCustomView(R.layout.design_tab_item_custom); tabLayout.addTab(tab); verify(mockListener, times(1)).onTabSelected(eq(tab)); verify(mockListener, times(0)).onTabUnselected(any(TabLayout.Tab.class)); assertNotNull("Tab has custom view", tab.getCustomView()); assertEquals("First tab is selected", 0, tabLayout.getSelectedTabPosition()); assertTabCustomViewSelected(tabLayout); } @Test @UiThreadTest public void testTabWithCustomLayoutSelection2() { final TabLayout.OnTabSelectedListener mockListener = mock(TabLayout.OnTabSelectedListener.class); final LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity()); final TabLayout tabLayout = (TabLayout) inflater.inflate(R.layout.design_tabs, null); tabLayout.addOnTabSelectedListener(mockListener); final TabLayout.Tab tab = tabLayout.newTab(); tabLayout.addTab(tab); verify(mockListener, times(1)).onTabSelected(eq(tab)); verify(mockListener, times(0)).onTabUnselected(any(TabLayout.Tab.class)); tab.setCustomView(R.layout.design_tab_item_custom); assertNotNull("Tab has custom view", tab.getCustomView()); assertEquals("First tab is selected", 0, tabLayout.getSelectedTabPosition()); assertTabCustomViewSelected(tabLayout); } @Test @UiThreadTest public void testMultipleTabsWithCustomLayoutSelection1() { final TabLayout.OnTabSelectedListener mockListener = mock(TabLayout.OnTabSelectedListener.class); final LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity()); final TabLayout tabs = (TabLayout) inflater.inflate(R.layout.design_tabs, null); tabs.addOnTabSelectedListener(mockListener); final TabLayout.Tab tab1 = tabs.newTab().setCustomView(R.layout.design_tab_item_custom); tabs.addTab(tab1); verify(mockListener, times(1)).onTabSelected(eq(tab1)); verify(mockListener, times(0)).onTabUnselected(any(TabLayout.Tab.class)); final TabLayout.Tab tab2 = tabs.newTab().setCustomView(R.layout.design_tab_item_custom); tabs.addTab(tab2, true); verify(mockListener, times(1)).onTabSelected(eq(tab2)); verify(mockListener, times(1)).onTabUnselected(eq(tab1)); final TabLayout.Tab tab3 = tabs.newTab().setCustomView(R.layout.design_tab_item_custom); tabs.addTab(tab3); verifyNoMoreInteractions(mockListener); assertEquals("Second tab is selected", 1, tabs.getSelectedTabPosition()); assertTabCustomViewSelected(tabs); } @Test @UiThreadTest public void testMultipleTabsWithCustomLayoutSelection2() { final TabLayout.OnTabSelectedListener mockListener = mock(TabLayout.OnTabSelectedListener.class); final LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity()); final TabLayout tabs = (TabLayout) inflater.inflate(R.layout.design_tabs, null); tabs.addOnTabSelectedListener(mockListener); final TabLayout.Tab tab1 = tabs.newTab(); tabs.addTab(tab1); verify(mockListener, times(1)).onTabSelected(eq(tab1)); verify(mockListener, times(0)).onTabUnselected(any(TabLayout.Tab.class)); final TabLayout.Tab tab2 = tabs.newTab(); tabs.addTab(tab2, true); verify(mockListener, times(1)).onTabSelected(eq(tab2)); verify(mockListener, times(1)).onTabUnselected(eq(tab1)); final TabLayout.Tab tab3 = tabs.newTab(); tabs.addTab(tab3); verifyNoMoreInteractions(mockListener); tabs.getTabAt(0).setCustomView(R.layout.design_tab_item_custom); tabs.getTabAt(1).setCustomView(R.layout.design_tab_item_custom); tabs.getTabAt(2).setCustomView(R.layout.design_tab_item_custom); assertEquals("Second tab is selected", 1, tabs.getSelectedTabPosition()); assertTabCustomViewSelected(tabs); } private static void assertTabCustomViewSelected(final TabLayout tabLayout) { for (int i = 0, count = tabLayout.getTabCount(); i < count; i++) { final TabLayout.Tab tab = tabLayout.getTabAt(i); final View tabCustomView = tab.getCustomView(); if (tabCustomView != null) { assertEquals(tab.isSelected(), tabCustomView.isSelected()); } } } @Test public void setScrollPositionLtr() throws Throwable { testSetScrollPosition(true); } @Test public void setScrollPositionRtl() throws Throwable { testSetScrollPosition(false); } private void testSetScrollPosition(final boolean isLtr) throws Throwable { mActivityTestRule.runOnUiThread(new Runnable() { @Override public void run() { mActivityTestRule.getActivity().setContentView(R.layout.design_tabs_fixed_width); } }); final TabLayout tabs = (TabLayout) mActivityTestRule.getActivity().findViewById(R.id.tabs); assertEquals(TabLayout.MODE_SCROLLABLE, tabs.getTabMode()); final TabLayoutScrollIdlingResource idler = new TabLayoutScrollIdlingResource(tabs); Espresso.registerIdlingResources(idler); // We're going to call setScrollPosition() incrementally, as if scrolling between one tab // and the next. Use the middle tab for best results. The positionOffsets should be in the // range [0, 1), so the final call will wrap to 0 but use the next tab's position. final int middleTab = tabs.getTabCount() / 2; final int[] positions = {middleTab, middleTab, middleTab, middleTab, middleTab + 1}; final float[] positionOffsets = {0f, .25f, .5f, .75f, 0f}; // Set layout direction onView(withId(R.id.tabs)).perform(setLayoutDirection( isLtr ? ViewCompat.LAYOUT_DIRECTION_LTR : ViewCompat.LAYOUT_DIRECTION_RTL)); // Make sure it's scrolled all the way to the start onView(withId(R.id.tabs)).perform(selectTab(0)); // Perform a series of setScrollPosition() calls final AtomicInteger lastScrollX = new AtomicInteger(tabs.getScrollX()); for (int i = 0; i < positions.length; i++) { onView(withId(R.id.tabs)) .perform(setScrollPosition(positions[i], positionOffsets[i])) .check(new ViewAssertion() { @Override public void check(View view, NoMatchingViewException notFoundException) { if (view == null) { throw notFoundException; } // Verify increasing or decreasing scroll X values int sx = view.getScrollX(); assertTrue(isLtr ? sx > lastScrollX.get() : sx < lastScrollX.get()); lastScrollX.set(sx); } }); } Espresso.unregisterIdlingResources(idler); } static class TabLayoutScrollIdlingResource implements IdlingResource { private boolean mIsIdle = true; private ResourceCallback mCallback; TabLayoutScrollIdlingResource(final TabLayout tabLayout) { tabLayout.setScrollAnimatorListener(new ValueAnimatorCompat.AnimatorListenerAdapter() { @Override public void onAnimationStart(ValueAnimatorCompat animator) { setIdle(false); } @Override public void onAnimationEnd(ValueAnimatorCompat animator) { setIdle(true); } }); } @Override public String getName() { return "TabLayoutScrollIdlingResource"; } @Override public boolean isIdleNow() { return mIsIdle; } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { mCallback = callback; } private void setIdle(boolean idle) { boolean wasIdle = mIsIdle; mIsIdle = idle; if (mIsIdle && !wasIdle && mCallback != null) { mCallback.onTransitionToIdle(); } } } }