Ga\u00ebtan Muller b3fe6a7f61 [Cleanup] Remove redundant version checks for api <= 19.
Resolves https://github.com/material-components/material-components-android/pull/3849

GIT_ORIGIN_REV_ID=47bb2fcc8031740e5a7d4f741f1ada2af025e126
PiperOrigin-RevId: 632154861
2024-05-09 18:46:02 +00:00

602 lines
22 KiB
Java

/*
* 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 com.google.android.material.tabs;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.google.android.material.testutils.TabLayoutActions.selectTab;
import static com.google.android.material.testutils.TabLayoutActions.setScrollPosition;
import static com.google.android.material.testutils.TabLayoutActions.setTabMode;
import static com.google.android.material.testutils.TestUtilsActions.setLayoutDirection;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.TargetApi;
import android.os.Build;
import androidx.appcompat.app.AppCompatActivity;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionInfoCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat;
import androidx.test.annotation.UiThreadTest;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.IdlingRegistry;
import androidx.test.espresso.IdlingResource;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.google.android.material.tabs.TabLayout.Tab;
import com.google.android.material.tabs.TabLayout.TabView;
import com.google.android.material.testapp.R;
import com.google.android.material.testutils.AccessibilityUtils;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TabLayoutTest {
@Rule
public final ActivityTestRule<AppCompatActivity> activityTestRule =
new ActivityTestRule<>(AppCompatActivity.class);
@Test
@UiThreadTest
public void testInflateTabLayoutWithTabItems() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.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(
activityTestRule.getActivity().getString(R.string.tab_layout_text),
tab.getText().toString());
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(activityTestRule.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(activityTestRule.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(activityTestRule.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
public void testTabWithIdIsFound() throws Throwable {
AppCompatActivity activity = activityTestRule.getActivity();
int id = ViewCompat.generateViewId();
activityTestRule.runOnUiThread(
new Runnable() {
@Override
public void run() {
activity.setContentView(R.layout.design_tabs);
TabLayout tabs = activity.findViewById(R.id.tabs);
TabLayout.Tab tab = tabs.newTab().setId(id).setText("test text");
tabs.addTab(tab);
}
});
Espresso.onIdle();
onView(withId(id)).check(matches(hasDescendant(withText(containsString("test text")))));
}
@Test
@UiThreadTest
public void testMultipleTabsWithCustomLayoutSelection1() {
final TabLayout.OnTabSelectedListener mockListener =
mock(TabLayout.OnTabSelectedListener.class);
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.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 testTabSelectionSelectTab() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.getActivity());
final TabLayout tabs = (TabLayout) inflater.inflate(R.layout.design_tabs, null);
final TabLayout.Tab tab1 = tabs.newTab();
tabs.addTab(tab1);
assertTrue(tab1.isSelected());
final TabLayout.Tab tab2 = tabs.newTab();
tabs.addTab(tab2);
assertFalse(tab2.isSelected());
final TabLayout.Tab tab3 = tabs.newTab();
tabs.addTab(tab3);
assertFalse(tab3.isSelected());
tabs.selectTab(tab3);
assertTrue(tab3.isSelected());
assertFalse(tab1.isSelected());
assertFalse(tab2.isSelected());
assertEquals(2, tabs.indicatorPosition);
tabs.selectTab(tab2);
assertTrue(tab2.isSelected());
assertFalse(tab1.isSelected());
assertFalse(tab3.isSelected());
assertEquals(1, tabs.indicatorPosition);
tabs.selectTab(tab1);
assertTrue(tab1.isSelected());
assertFalse(tab2.isSelected());
assertFalse(tab3.isSelected());
assertEquals(0, tabs.indicatorPosition);
}
@Test
@UiThreadTest
public void testTabSelectionAddTab() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.getActivity());
final TabLayout tabs = (TabLayout) inflater.inflate(R.layout.design_tabs, null);
final TabLayout.Tab tab1 = tabs.newTab();
tabs.addTab(tab1);
assertTrue(tab1.isSelected());
final TabLayout.Tab tab2 = tabs.newTab();
tabs.addTab(tab2, false);
assertFalse(tab2.isSelected());
final TabLayout.Tab tab3 = tabs.newTab();
tabs.addTab(tab3, true);
assertTrue(tab3.isSelected());
final TabLayout.Tab tab4 = tabs.newTab();
tabs.addTab(tab4);
assertFalse(tab4.isSelected());
}
@Test
@UiThreadTest
public void testTabSelectionRemoveTab() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.getActivity());
final TabLayout tabs = (TabLayout) inflater.inflate(R.layout.design_tabs, null);
final TabLayout.Tab tab1 = tabs.newTab();
tabs.addTab(tab1);
assertTrue(tab1.isSelected());
final TabLayout.Tab tab2 = tabs.newTab();
tabs.addTab(tab2);
assertFalse(tab2.isSelected());
final TabLayout.Tab tab3 = tabs.newTab();
tabs.addTab(tab3, true);
assertTrue(tab3.isSelected());
tabs.removeTab(tab3);
assertTrue(tab2.isSelected());
tabs.removeTab(tab1);
assertTrue(tab2.isSelected());
}
@Test
@UiThreadTest
public void testTabSelectionNewTab() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.getActivity());
final TabLayout tabs = (TabLayout) inflater.inflate(R.layout.design_tabs, null);
final TabLayout.Tab tab1 = tabs.newTab();
assertFalse(tab1.isSelected());
tabs.addTab(tab1);
assertTrue(tab1.isSelected());
tabs.addTab(tabs.newTab());
tabs.addTab(tabs.newTab());
tabs.removeAllTabs();
assertFalse(tabs.newTab().isSelected());
}
@Test
@UiThreadTest
public void testMultipleTabsWithCustomLayoutSelection2() {
final TabLayout.OnTabSelectedListener mockListener =
mock(TabLayout.OnTabSelectedListener.class);
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.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);
}
@Test
@UiThreadTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
@TargetApi(Build.VERSION_CODES.N)
public void testPointerIcon() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.getActivity());
final TabLayout tabLayout = (TabLayout) inflater.inflate(R.layout.design_tabs_items, null);
final PointerIcon expectedIcon =
PointerIcon.getSystemIcon(activityTestRule.getActivity(), PointerIcon.TYPE_HAND);
final int tabCount = tabLayout.getTabCount();
assertEquals(3, tabCount);
final MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0);
for (int i = 0; i < tabCount; i++) {
assertEquals(expectedIcon, tabLayout.getTabAt(i).view.onResolvePointerIcon(event, 0));
}
}
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);
}
@Test
public void testModeAuto() throws Throwable {
activityTestRule.runOnUiThread(
() -> activityTestRule.getActivity().setContentView(R.layout.design_tabs_fixed_width));
final TabLayout tabs = activityTestRule.getActivity().findViewById(R.id.tabs);
final TabLayoutScrollIdlingResource idler = new TabLayoutScrollIdlingResource(tabs);
IdlingRegistry.getInstance().register(idler);
onView(withId(R.id.tabs)).perform(setTabMode(TabLayout.MODE_AUTO));
// Make sure tabs are scrolled all the way to the start
onView(withId(R.id.tabs)).perform(selectTab(0));
onView(withId(R.id.tabs))
.check(
(view, notFoundException) -> {
if (!(view instanceof TabLayout)) {
throw notFoundException;
}
TabLayout tabs1 = (TabLayout) view;
assertEquals(TabLayout.MODE_AUTO, tabs1.getTabMode());
int tabWidth = 0;
for (int i = 0; i < tabs1.getTabCount(); i++) {
Tab tab = tabs1.getTabAt(i);
tabWidth += tab.view.getMeasuredWidth();
}
// In MODE_AUTO, the total width of tabs can exceed the width of the parent
// TabLayout
assertTrue(tabWidth > tabs1.getMeasuredWidth());
});
// Make sure tabs are scrolled all the way to the end
onView(withId(R.id.tabs))
.perform(selectTab(7))
.check(
(view, notFoundException) -> {
if (!(view instanceof TabLayout)) {
throw notFoundException;
}
assertTrue(view.getScrollX() > view.getMeasuredWidth());
});
IdlingRegistry.getInstance().unregister(idler);
}
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
public void initializesAccessibilityNodeInfo() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.getActivity());
final TabLayout tabs = (TabLayout) inflater.inflate(R.layout.design_tabs, null);
final TabLayout.Tab tab1 = tabs.newTab();
tabs.addTab(tab1);
final TabLayout.Tab tab2 = tabs.newTab();
tabs.addTab(tab2, true);
tabs.getTabAt(0).setCustomView(R.layout.design_tab_item_custom);
tabs.getTabAt(1).setCustomView(R.layout.design_tab_item_custom);
AccessibilityNodeInfoCompat groupInfoCompat = AccessibilityNodeInfoCompat.obtain();
ViewCompat.onInitializeAccessibilityNodeInfo(tabs, groupInfoCompat);
CollectionInfoCompat collectionInfo = groupInfoCompat.getCollectionInfo();
assertEquals(2, collectionInfo.getColumnCount());
assertEquals(1, collectionInfo.getRowCount());
TabView secondChild = tabs.getTabAt(1).view;
secondChild.setSelected(true);
AccessibilityNodeInfoCompat tabInfoCompat = AccessibilityNodeInfoCompat.obtain();
ViewCompat.onInitializeAccessibilityNodeInfo(secondChild, tabInfoCompat);
// A tab that is currently selected won't be clickable
assertFalse(tabInfoCompat.isClickable());
assertFalse(
AccessibilityUtils.hasAction(tabInfoCompat, AccessibilityActionCompat.ACTION_CLICK));
CollectionItemInfoCompat itemInfo = tabInfoCompat.getCollectionItemInfo();
assertEquals(1, itemInfo.getColumnIndex());
assertEquals(0, itemInfo.getRowIndex());
assertTrue(itemInfo.isSelected());
}
private void testSetScrollPosition(final boolean isLtr) throws Throwable {
activityTestRule.runOnUiThread(
() -> activityTestRule.getActivity().setContentView(R.layout.design_tabs_fixed_width));
final TabLayout tabs = activityTestRule.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(
(view, 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);
}
@Test
@UiThreadTest
public void testSetCustomTabReplacesCustomView() {
final LayoutInflater inflater = LayoutInflater.from(activityTestRule.getActivity());
final TabLayout tabLayout = (TabLayout) inflater.inflate(R.layout.design_tabs, null);
final TabLayout.Tab tab = tabLayout.newTab();
tab.setCustomView(R.layout.design_tab_item_custom);
tabLayout.addTab(tab);
tab.setCustomView(R.layout.design_tab_item_custom_alternate);
assertNull(tabLayout.findViewById(R.id.my_custom_tab));
assertNotNull(tabLayout.findViewById(R.id.my_custom_alternate_tab));
}
/**
* Tests that the indicator animation still functions as intended when modifying the animator's
* update listener, instead of removing/recreating the animator itself.
*/
@Test
public void testIndicatorAnimator_worksAfterReplacingUpdateListener() throws Throwable {
activityTestRule.runOnUiThread(
() -> activityTestRule.getActivity().setContentView(R.layout.design_tabs_items));
final TabLayout tabs = activityTestRule.getActivity().findViewById(R.id.tabs);
onView(withId(R.id.tabs)).perform(setTabMode(TabLayout.MODE_FIXED));
final TabLayoutScrollIdlingResource idler = new TabLayoutScrollIdlingResource(tabs);
IdlingRegistry.getInstance().register(idler);
// We need to click a tab once to set up the indicator animation (so that it's not still null).
onView(withId(R.id.tabs)).perform(selectTab(1));
// Select new tab. This action should modify the listener on the animator.
onView(withId(R.id.tabs)).perform(selectTab(2));
onView(withId(R.id.tabs))
.check(
(view, notFoundException) -> {
if (view == null) {
throw notFoundException;
}
TabLayout tabs1 = (TabLayout) view;
int tabTwoLeft = tabs1.getTabAt(/* index= */ 2).view.getLeft();
int tabTwoRight = tabs1.getTabAt(/* index= */ 2).view.getRight();
assertEquals(tabs1.tabSelectedIndicator.getBounds().left, tabTwoLeft);
assertEquals(tabs1.tabSelectedIndicator.getBounds().right, tabTwoRight);
assertEquals(2, tabs.indicatorPosition);
});
IdlingRegistry.getInstance().unregister(idler);
}
static class TabLayoutScrollIdlingResource implements IdlingResource {
private boolean isIdle = true;
private ResourceCallback callback;
TabLayoutScrollIdlingResource(final TabLayout tabLayout) {
tabLayout.setScrollAnimatorListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animator) {
setIdle(false);
}
@Override
public void onAnimationEnd(Animator animator) {
setIdle(true);
}
});
}
@Override
public String getName() {
return "TabLayoutScrollIdlingResource";
}
@Override
public boolean isIdleNow() {
return isIdle;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
private void setIdle(boolean idle) {
boolean wasIdle = isIdle;
isIdle = idle;
if (isIdle && !wasIdle && callback != null) {
callback.onTransitionToIdle();
}
}
}
}