/* * 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.BottomNavigationViewActions.setIconForMenuItem; import static android.support.design.testutils.BottomNavigationViewActions.setItemIconTintList; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.core.AllOf.allOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.res.Resources; import android.os.Parcelable; import android.support.annotation.ColorInt; import android.support.design.test.R; import android.support.design.testutils.TestDrawable; import android.support.design.testutils.TestUtilsMatchers; import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.support.v4.content.res.ResourcesCompat; import android.view.Menu; import android.view.MenuItem; import org.junit.Before; import org.junit.Test; import java.util.HashMap; import java.util.Map; public class BottomNavigationViewTest extends BaseInstrumentationTestCase<BottomNavigationViewActivity> { private static final int[] MENU_CONTENT_ITEM_IDS = { R.id.destination_home, R.id.destination_profile, R.id.destination_people }; private Map<Integer, String> mMenuStringContent; private BottomNavigationView mBottomNavigation; public BottomNavigationViewTest() { super(BottomNavigationViewActivity.class); } @Before public void setUp() throws Exception { final BottomNavigationViewActivity activity = mActivityTestRule.getActivity(); mBottomNavigation = (BottomNavigationView) activity.findViewById(R.id.bottom_navigation); final Resources res = activity.getResources(); mMenuStringContent = new HashMap<>(MENU_CONTENT_ITEM_IDS.length); mMenuStringContent.put(R.id.destination_home, res.getString(R.string.navigate_home)); mMenuStringContent.put(R.id.destination_profile, res.getString(R.string.navigate_profile)); mMenuStringContent.put(R.id.destination_people, res.getString(R.string.navigate_people)); } @UiThreadTest @Test @SmallTest public void testAddItemsWithoutMenuInflation() { BottomNavigationView navigation = new BottomNavigationView(mActivityTestRule.getActivity()); mActivityTestRule.getActivity().setContentView(navigation); navigation.getMenu().add("Item1"); navigation.getMenu().add("Item2"); assertEquals(2, navigation.getMenu().size()); navigation.getMenu().removeItem(0); navigation.getMenu().removeItem(0); assertEquals(0, navigation.getMenu().size()); } @Test @SmallTest public void testBasics() { // Check the contents of the Menu object final Menu menu = mBottomNavigation.getMenu(); assertNotNull("Menu should not be null", menu); assertEquals("Should have matching number of items", MENU_CONTENT_ITEM_IDS.length, menu.size()); for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { final MenuItem currItem = menu.getItem(i); assertEquals("ID for Item #" + i, MENU_CONTENT_ITEM_IDS[i], currItem.getItemId()); } } @Test @SmallTest public void testNavigationSelectionListener() { BottomNavigationView.OnNavigationItemSelectedListener mockedListener = mock(BottomNavigationView.OnNavigationItemSelectedListener.class); mBottomNavigation.setOnNavigationItemSelectedListener(mockedListener); // Make the listener return true to allow selecting the item. when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true); onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify our listener has been notified of the click verify(mockedListener, times(1)).onNavigationItemSelected( mBottomNavigation.getMenu().findItem(R.id.destination_profile)); // Verify the item is now selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Select the same item again onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify our listener has been notified of the click verify(mockedListener, times(2)).onNavigationItemSelected( mBottomNavigation.getMenu().findItem(R.id.destination_profile)); // Verify the item is still selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Make the listener return false to disallow selecting the item. when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false); onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify our listener has been notified of the click verify(mockedListener, times(1)).onNavigationItemSelected( mBottomNavigation.getMenu().findItem(R.id.destination_people)); // Verify the previous item is still selected assertFalse(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Set null listener to test that the next click is not going to notify the // previously set listener and will allow selecting items. mBottomNavigation.setOnNavigationItemSelectedListener(null); // Click one of our items onView(allOf(withText(mMenuStringContent.get(R.id.destination_home)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify that our previous listener has not been notified of the click verifyNoMoreInteractions(mockedListener); // Verify the correct item is now selected. assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_home).isChecked()); } @UiThreadTest @Test @SmallTest public void testSetSelectedItemId() { BottomNavigationView.OnNavigationItemSelectedListener mockedListener = mock(BottomNavigationView.OnNavigationItemSelectedListener.class); mBottomNavigation.setOnNavigationItemSelectedListener(mockedListener); // Make the listener return true to allow selecting the item. when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true); // Programmatically select an item mBottomNavigation.setSelectedItemId(R.id.destination_profile); // Verify our listener has been notified of the click verify(mockedListener, times(1)).onNavigationItemSelected( mBottomNavigation.getMenu().findItem(R.id.destination_profile)); // Verify the item is now selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Select the same item mBottomNavigation.setSelectedItemId(R.id.destination_profile); // Verify our listener has been notified of the click verify(mockedListener, times(2)).onNavigationItemSelected( mBottomNavigation.getMenu().findItem(R.id.destination_profile)); // Verify the item is still selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Make the listener return false to disallow selecting the item. when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false); // Programmatically select an item mBottomNavigation.setSelectedItemId(R.id.destination_people); // Verify our listener has been notified of the click verify(mockedListener, times(1)).onNavigationItemSelected( mBottomNavigation.getMenu().findItem(R.id.destination_people)); // Verify the previous item is still selected assertFalse(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Set null listener to test that the next click is not going to notify the // previously set listener and will allow selecting items. mBottomNavigation.setOnNavigationItemSelectedListener(null); // Select one of our items mBottomNavigation.setSelectedItemId(R.id.destination_home); // Verify that our previous listener has not been notified of the click verifyNoMoreInteractions(mockedListener); // Verify the correct item is now selected. assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_home).isChecked()); } @Test @SmallTest public void testNavigationReselectionListener() { // Add an OnNavigationItemReselectedListener BottomNavigationView.OnNavigationItemReselectedListener reselectedListener = mock(BottomNavigationView.OnNavigationItemReselectedListener.class); mBottomNavigation.setOnNavigationItemReselectedListener(reselectedListener); // Select an item onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify the item is now selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Verify the listener was not called verify(reselectedListener, never()).onNavigationItemReselected(any(MenuItem.class)); // Select the same item again onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify the item is still selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Verify the listener was called verify(reselectedListener, times(1)).onNavigationItemReselected( mBottomNavigation.getMenu().findItem(R.id.destination_profile)); // Add an OnNavigationItemSelectedListener BottomNavigationView.OnNavigationItemSelectedListener selectedListener = mock(BottomNavigationView.OnNavigationItemSelectedListener.class); mBottomNavigation.setOnNavigationItemSelectedListener(selectedListener); // Make the listener return true to allow selecting the item. when(selectedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true); // Select another item onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify the item is now selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); // Verify the correct listeners were called verify(selectedListener, times(1)).onNavigationItemSelected( mBottomNavigation.getMenu().findItem(R.id.destination_people)); verify(reselectedListener, never()).onNavigationItemReselected( mBottomNavigation.getMenu().findItem(R.id.destination_people)); // Select the same item again onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify the item is still selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); // Verify the correct listeners were called verifyNoMoreInteractions(selectedListener); verify(reselectedListener, times(1)).onNavigationItemReselected( mBottomNavigation.getMenu().findItem(R.id.destination_people)); // Remove the OnNavigationItemReselectedListener mBottomNavigation.setOnNavigationItemReselectedListener(null); // Select the same item again onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); // Verify the item is still selected assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); // Verify the reselectedListener was not called verifyNoMoreInteractions(reselectedListener); } @UiThreadTest @Test @SmallTest public void testSelectedItemIdWithEmptyMenu() { // First item initially selected assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId()); // Remove all the items for (int id : mMenuStringContent.keySet()) { mBottomNavigation.getMenu().removeItem(id); } // Verify selected ID is zero assertEquals(0, mBottomNavigation.getSelectedItemId()); // Add an item mBottomNavigation.getMenu().add(0, R.id.destination_home, 0, R.string.navigate_home); // Verify item is selected assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId()); // Try selecting an invalid ID mBottomNavigation.setSelectedItemId(R.id.destination_people); // Verify the view has not changed assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId()); } @Test @SmallTest public void testIconTinting() { final Resources res = mActivityTestRule.getActivity().getResources(); @ColorInt final int redFill = ResourcesCompat.getColor(res, R.color.test_red, null); @ColorInt final int greenFill = ResourcesCompat.getColor(res, R.color.test_green, null); @ColorInt final int blueFill = ResourcesCompat.getColor(res, R.color.test_blue, null); final int iconSize = res.getDimensionPixelSize(R.dimen.drawable_small_size); onView(withId(R.id.bottom_navigation)).perform(setIconForMenuItem(R.id.destination_home, new TestDrawable(redFill, iconSize, iconSize))); onView(withId(R.id.bottom_navigation)).perform(setIconForMenuItem(R.id.destination_profile, new TestDrawable(greenFill, iconSize, iconSize))); onView(withId(R.id.bottom_navigation)).perform(setIconForMenuItem(R.id.destination_people, new TestDrawable(blueFill, iconSize, iconSize))); @ColorInt final int defaultTintColor = ResourcesCompat.getColor(res, R.color.emerald_translucent, null); // We're allowing a margin of error in checking the color of the items' icons. // This is due to the translucent color being used in the icon tinting // and off-by-one discrepancies of SRC_IN when it's compositing // translucent color. Note that all the checks below are written for the current // logic on BottomNavigationView that uses the default SRC_IN tint mode - effectively // replacing all non-transparent pixels in the destination (original icon) with // our translucent tint color. final int allowedComponentVariance = 1; // Note that here we're tying ourselves to the implementation details of the internal // structure of the BottomNavigationView. Specifically, we're checking the drawable the // ImageView with id R.id.icon. If the internal implementation of BottomNavigationView // changes, the second Matcher in the lookups below will need to be tweaked. onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_home)))).check( matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance))); onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_profile)))).check( matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance))); onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_people)))).check( matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance))); @ColorInt final int newTintColor = ResourcesCompat.getColor(res, R.color.red_translucent, null); onView(withId(R.id.bottom_navigation)).perform(setItemIconTintList( ResourcesCompat.getColorStateList(res, R.color.color_state_list_red_translucent, null))); // Check that all menu items with icons now have icons tinted with the newly set color onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_home)))).check( matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance))); onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_profile)))).check( matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance))); onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_people)))).check( matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance))); // And now remove all icon tinting onView(withId(R.id.bottom_navigation)).perform(setItemIconTintList(null)); // And verify that all menu items with icons now have the original colors for their icons. // Note that since there is no tinting at this point, we don't allow any color variance // in these checks. onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_home)))).check( matches(TestUtilsMatchers.drawable(redFill, allowedComponentVariance))); onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_profile)))).check( matches(TestUtilsMatchers.drawable(greenFill, allowedComponentVariance))); onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_people)))).check( matches(TestUtilsMatchers.drawable(blueFill, allowedComponentVariance))); } @UiThreadTest @Test @SmallTest public void testItemChecking() throws Throwable { final Menu menu = mBottomNavigation.getMenu(); assertTrue(menu.getItem(0).isChecked()); checkAndVerifyExclusiveItem(menu, R.id.destination_home); checkAndVerifyExclusiveItem(menu, R.id.destination_profile); checkAndVerifyExclusiveItem(menu, R.id.destination_people); } @UiThreadTest @Test @SmallTest public void testClearingMenu() throws Throwable { mBottomNavigation.getMenu().clear(); assertEquals(0, mBottomNavigation.getMenu().size()); mBottomNavigation.inflateMenu(R.menu.bottom_navigation_view_content); assertEquals(3, mBottomNavigation.getMenu().size()); } @Test @SmallTest public void testSavedState() throws Throwable { // Select an item other than the first onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); // Save the state final Parcelable state = mBottomNavigation.onSaveInstanceState(); // Restore the state into a fresh BottomNavigationView mActivityTestRule.runOnUiThread(new Runnable() { @Override public void run() { BottomNavigationView testView = new BottomNavigationView(mActivityTestRule.getActivity()); testView.inflateMenu(R.menu.bottom_navigation_view_content); testView.onRestoreInstanceState(state); assertTrue(testView.getMenu().findItem(R.id.destination_profile).isChecked()); } }); } private void checkAndVerifyExclusiveItem(final Menu menu, final int id) throws Throwable { menu.findItem(id).setChecked(true); for (int i = 0; i < menu.size(); i++) { final MenuItem item = menu.getItem(i); if (item.getItemId() == id) { assertTrue(item.isChecked()); } else { assertFalse(item.isChecked()); } } } }