1/* 2 * Copyright (C) 2016 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 static android.support.design.testutils.BottomNavigationViewActions.setIconForMenuItem; 19import static android.support.design.testutils.BottomNavigationViewActions.setItemIconTintList; 20import static android.support.test.espresso.Espresso.onView; 21import static android.support.test.espresso.action.ViewActions.click; 22import static android.support.test.espresso.assertion.ViewAssertions.matches; 23import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; 24import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 25import static android.support.test.espresso.matcher.ViewMatchers.withId; 26import static android.support.test.espresso.matcher.ViewMatchers.withText; 27 28import static org.hamcrest.core.AllOf.allOf; 29import static org.junit.Assert.assertEquals; 30import static org.junit.Assert.assertFalse; 31import static org.junit.Assert.assertNotNull; 32import static org.junit.Assert.assertTrue; 33import static org.mockito.Matchers.any; 34import static org.mockito.Mockito.mock; 35import static org.mockito.Mockito.never; 36import static org.mockito.Mockito.times; 37import static org.mockito.Mockito.verify; 38import static org.mockito.Mockito.verifyNoMoreInteractions; 39import static org.mockito.Mockito.when; 40 41import android.app.Activity; 42import android.content.res.Resources; 43import android.os.Build; 44import android.os.Parcelable; 45import android.support.annotation.ColorInt; 46import android.support.design.test.R; 47import android.support.design.testutils.TestDrawable; 48import android.support.design.testutils.TestUtilsMatchers; 49import android.support.test.annotation.UiThreadTest; 50import android.support.test.filters.LargeTest; 51import android.support.test.filters.SdkSuppress; 52import android.support.test.filters.SmallTest; 53import android.support.v4.content.res.ResourcesCompat; 54import android.view.Menu; 55import android.view.MenuItem; 56import android.view.MotionEvent; 57import android.view.PointerIcon; 58import android.view.View; 59import android.view.ViewGroup; 60 61import org.junit.Before; 62import org.junit.Test; 63 64import java.util.HashMap; 65import java.util.Map; 66 67public class BottomNavigationViewTest 68 extends BaseInstrumentationTestCase<BottomNavigationViewActivity> { 69 private static final int[] MENU_CONTENT_ITEM_IDS = { R.id.destination_home, 70 R.id.destination_profile, R.id.destination_people }; 71 private Map<Integer, String> mMenuStringContent; 72 73 private BottomNavigationView mBottomNavigation; 74 75 public BottomNavigationViewTest() { 76 super(BottomNavigationViewActivity.class); 77 } 78 79 @Before 80 public void setUp() throws Exception { 81 final BottomNavigationViewActivity activity = mActivityTestRule.getActivity(); 82 mBottomNavigation = (BottomNavigationView) activity.findViewById(R.id.bottom_navigation); 83 84 final Resources res = activity.getResources(); 85 mMenuStringContent = new HashMap<>(MENU_CONTENT_ITEM_IDS.length); 86 mMenuStringContent.put(R.id.destination_home, res.getString(R.string.navigate_home)); 87 mMenuStringContent.put(R.id.destination_profile, res.getString(R.string.navigate_profile)); 88 mMenuStringContent.put(R.id.destination_people, res.getString(R.string.navigate_people)); 89 } 90 91 @UiThreadTest 92 @Test 93 @SmallTest 94 public void testAddItemsWithoutMenuInflation() { 95 BottomNavigationView navigation = new BottomNavigationView(mActivityTestRule.getActivity()); 96 mActivityTestRule.getActivity().setContentView(navigation); 97 navigation.getMenu().add("Item1"); 98 navigation.getMenu().add("Item2"); 99 assertEquals(2, navigation.getMenu().size()); 100 navigation.getMenu().removeItem(0); 101 navigation.getMenu().removeItem(0); 102 assertEquals(0, navigation.getMenu().size()); 103 } 104 105 @Test 106 @SmallTest 107 public void testBasics() { 108 // Check the contents of the Menu object 109 final Menu menu = mBottomNavigation.getMenu(); 110 assertNotNull("Menu should not be null", menu); 111 assertEquals("Should have matching number of items", MENU_CONTENT_ITEM_IDS.length, 112 menu.size()); 113 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 114 final MenuItem currItem = menu.getItem(i); 115 assertEquals("ID for Item #" + i, MENU_CONTENT_ITEM_IDS[i], currItem.getItemId()); 116 } 117 118 } 119 120 @Test 121 @LargeTest 122 public void testNavigationSelectionListener() { 123 BottomNavigationView.OnNavigationItemSelectedListener mockedListener = 124 mock(BottomNavigationView.OnNavigationItemSelectedListener.class); 125 mBottomNavigation.setOnNavigationItemSelectedListener(mockedListener); 126 127 // Make the listener return true to allow selecting the item. 128 when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true); 129 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 130 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 131 // Verify our listener has been notified of the click 132 verify(mockedListener, times(1)).onNavigationItemSelected( 133 mBottomNavigation.getMenu().findItem(R.id.destination_profile)); 134 // Verify the item is now selected 135 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 136 137 // Select the same item again 138 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 139 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 140 // Verify our listener has been notified of the click 141 verify(mockedListener, times(2)).onNavigationItemSelected( 142 mBottomNavigation.getMenu().findItem(R.id.destination_profile)); 143 // Verify the item is still selected 144 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 145 146 // Make the listener return false to disallow selecting the item. 147 when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false); 148 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 149 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 150 // Verify our listener has been notified of the click 151 verify(mockedListener, times(1)).onNavigationItemSelected( 152 mBottomNavigation.getMenu().findItem(R.id.destination_people)); 153 // Verify the previous item is still selected 154 assertFalse(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); 155 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 156 157 // Set null listener to test that the next click is not going to notify the 158 // previously set listener and will allow selecting items. 159 mBottomNavigation.setOnNavigationItemSelectedListener(null); 160 161 // Click one of our items 162 onView(allOf(withText(mMenuStringContent.get(R.id.destination_home)), 163 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 164 // Verify that our previous listener has not been notified of the click 165 verifyNoMoreInteractions(mockedListener); 166 // Verify the correct item is now selected. 167 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_home).isChecked()); 168 } 169 170 @UiThreadTest 171 @Test 172 @SmallTest 173 public void testSetSelectedItemId() { 174 BottomNavigationView.OnNavigationItemSelectedListener mockedListener = 175 mock(BottomNavigationView.OnNavigationItemSelectedListener.class); 176 mBottomNavigation.setOnNavigationItemSelectedListener(mockedListener); 177 178 // Make the listener return true to allow selecting the item. 179 when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true); 180 // Programmatically select an item 181 mBottomNavigation.setSelectedItemId(R.id.destination_profile); 182 // Verify our listener has been notified of the click 183 verify(mockedListener, times(1)).onNavigationItemSelected( 184 mBottomNavigation.getMenu().findItem(R.id.destination_profile)); 185 // Verify the item is now selected 186 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 187 188 // Select the same item 189 mBottomNavigation.setSelectedItemId(R.id.destination_profile); 190 // Verify our listener has been notified of the click 191 verify(mockedListener, times(2)).onNavigationItemSelected( 192 mBottomNavigation.getMenu().findItem(R.id.destination_profile)); 193 // Verify the item is still selected 194 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 195 196 // Make the listener return false to disallow selecting the item. 197 when(mockedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(false); 198 // Programmatically select an item 199 mBottomNavigation.setSelectedItemId(R.id.destination_people); 200 // Verify our listener has been notified of the click 201 verify(mockedListener, times(1)).onNavigationItemSelected( 202 mBottomNavigation.getMenu().findItem(R.id.destination_people)); 203 // Verify the previous item is still selected 204 assertFalse(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); 205 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 206 207 // Set null listener to test that the next click is not going to notify the 208 // previously set listener and will allow selecting items. 209 mBottomNavigation.setOnNavigationItemSelectedListener(null); 210 211 // Select one of our items 212 mBottomNavigation.setSelectedItemId(R.id.destination_home); 213 // Verify that our previous listener has not been notified of the click 214 verifyNoMoreInteractions(mockedListener); 215 // Verify the correct item is now selected. 216 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_home).isChecked()); 217 } 218 219 @Test 220 @SmallTest 221 public void testNavigationReselectionListener() { 222 // Add an OnNavigationItemReselectedListener 223 BottomNavigationView.OnNavigationItemReselectedListener reselectedListener = 224 mock(BottomNavigationView.OnNavigationItemReselectedListener.class); 225 mBottomNavigation.setOnNavigationItemReselectedListener(reselectedListener); 226 227 // Select an item 228 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 229 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 230 // Verify the item is now selected 231 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 232 // Verify the listener was not called 233 verify(reselectedListener, never()).onNavigationItemReselected(any(MenuItem.class)); 234 235 // Select the same item again 236 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 237 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 238 // Verify the item is still selected 239 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 240 // Verify the listener was called 241 verify(reselectedListener, times(1)).onNavigationItemReselected( 242 mBottomNavigation.getMenu().findItem(R.id.destination_profile)); 243 244 // Add an OnNavigationItemSelectedListener 245 BottomNavigationView.OnNavigationItemSelectedListener selectedListener = 246 mock(BottomNavigationView.OnNavigationItemSelectedListener.class); 247 mBottomNavigation.setOnNavigationItemSelectedListener(selectedListener); 248 // Make the listener return true to allow selecting the item. 249 when(selectedListener.onNavigationItemSelected(any(MenuItem.class))).thenReturn(true); 250 251 // Select another item 252 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 253 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 254 // Verify the item is now selected 255 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); 256 // Verify the correct listeners were called 257 verify(selectedListener, times(1)).onNavigationItemSelected( 258 mBottomNavigation.getMenu().findItem(R.id.destination_people)); 259 verify(reselectedListener, never()).onNavigationItemReselected( 260 mBottomNavigation.getMenu().findItem(R.id.destination_people)); 261 262 // Select the same item again 263 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 264 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 265 // Verify the item is still selected 266 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); 267 // Verify the correct listeners were called 268 verifyNoMoreInteractions(selectedListener); 269 verify(reselectedListener, times(1)).onNavigationItemReselected( 270 mBottomNavigation.getMenu().findItem(R.id.destination_people)); 271 272 // Remove the OnNavigationItemReselectedListener 273 mBottomNavigation.setOnNavigationItemReselectedListener(null); 274 275 // Select the same item again 276 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 277 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 278 // Verify the item is still selected 279 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_people).isChecked()); 280 // Verify the reselectedListener was not called 281 verifyNoMoreInteractions(reselectedListener); 282 } 283 284 @UiThreadTest 285 @Test 286 @SmallTest 287 public void testSelectedItemIdWithEmptyMenu() { 288 // First item initially selected 289 assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId()); 290 291 // Remove all the items 292 for (int id : mMenuStringContent.keySet()) { 293 mBottomNavigation.getMenu().removeItem(id); 294 } 295 // Verify selected ID is zero 296 assertEquals(0, mBottomNavigation.getSelectedItemId()); 297 298 // Add an item 299 mBottomNavigation.getMenu().add(0, R.id.destination_home, 0, R.string.navigate_home); 300 // Verify item is selected 301 assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId()); 302 303 // Try selecting an invalid ID 304 mBottomNavigation.setSelectedItemId(R.id.destination_people); 305 // Verify the view has not changed 306 assertEquals(R.id.destination_home, mBottomNavigation.getSelectedItemId()); 307 } 308 309 @Test 310 @SmallTest 311 public void testIconTinting() { 312 final Resources res = mActivityTestRule.getActivity().getResources(); 313 @ColorInt final int redFill = ResourcesCompat.getColor(res, R.color.test_red, null); 314 @ColorInt final int greenFill = ResourcesCompat.getColor(res, R.color.test_green, null); 315 @ColorInt final int blueFill = ResourcesCompat.getColor(res, R.color.test_blue, null); 316 final int iconSize = res.getDimensionPixelSize(R.dimen.drawable_small_size); 317 onView(withId(R.id.bottom_navigation)).perform(setIconForMenuItem(R.id.destination_home, 318 new TestDrawable(redFill, iconSize, iconSize))); 319 onView(withId(R.id.bottom_navigation)).perform(setIconForMenuItem(R.id.destination_profile, 320 new TestDrawable(greenFill, iconSize, iconSize))); 321 onView(withId(R.id.bottom_navigation)).perform(setIconForMenuItem(R.id.destination_people, 322 new TestDrawable(blueFill, iconSize, iconSize))); 323 324 @ColorInt final int defaultTintColor = ResourcesCompat.getColor(res, 325 R.color.emerald_translucent, null); 326 327 // We're allowing a margin of error in checking the color of the items' icons. 328 // This is due to the translucent color being used in the icon tinting 329 // and off-by-one discrepancies of SRC_IN when it's compositing 330 // translucent color. Note that all the checks below are written for the current 331 // logic on BottomNavigationView that uses the default SRC_IN tint mode - effectively 332 // replacing all non-transparent pixels in the destination (original icon) with 333 // our translucent tint color. 334 final int allowedComponentVariance = 1; 335 336 // Note that here we're tying ourselves to the implementation details of the internal 337 // structure of the BottomNavigationView. Specifically, we're checking the drawable the 338 // ImageView with id R.id.icon. If the internal implementation of BottomNavigationView 339 // changes, the second Matcher in the lookups below will need to be tweaked. 340 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_home)))).check( 341 matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance))); 342 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_profile)))).check( 343 matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance))); 344 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_people)))).check( 345 matches(TestUtilsMatchers.drawable(defaultTintColor, allowedComponentVariance))); 346 347 @ColorInt final int newTintColor = ResourcesCompat.getColor(res, 348 R.color.red_translucent, null); 349 onView(withId(R.id.bottom_navigation)).perform(setItemIconTintList( 350 ResourcesCompat.getColorStateList(res, R.color.color_state_list_red_translucent, 351 null))); 352 // Check that all menu items with icons now have icons tinted with the newly set color 353 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_home)))).check( 354 matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance))); 355 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_profile)))).check( 356 matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance))); 357 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_people)))).check( 358 matches(TestUtilsMatchers.drawable(newTintColor, allowedComponentVariance))); 359 360 // And now remove all icon tinting 361 onView(withId(R.id.bottom_navigation)).perform(setItemIconTintList(null)); 362 // And verify that all menu items with icons now have the original colors for their icons. 363 // Note that since there is no tinting at this point, we don't allow any color variance 364 // in these checks. 365 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_home)))).check( 366 matches(TestUtilsMatchers.drawable(redFill, allowedComponentVariance))); 367 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_profile)))).check( 368 matches(TestUtilsMatchers.drawable(greenFill, allowedComponentVariance))); 369 onView(allOf(withId(R.id.icon), isDescendantOfA(withId(R.id.destination_people)))).check( 370 matches(TestUtilsMatchers.drawable(blueFill, allowedComponentVariance))); 371 } 372 373 @UiThreadTest 374 @Test 375 @SmallTest 376 public void testItemChecking() throws Throwable { 377 final Menu menu = mBottomNavigation.getMenu(); 378 assertTrue(menu.getItem(0).isChecked()); 379 checkAndVerifyExclusiveItem(menu, R.id.destination_home); 380 checkAndVerifyExclusiveItem(menu, R.id.destination_profile); 381 checkAndVerifyExclusiveItem(menu, R.id.destination_people); 382 } 383 384 @UiThreadTest 385 @Test 386 @SmallTest 387 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 388 public void testPointerIcon() throws Throwable { 389 final Activity activity = mActivityTestRule.getActivity(); 390 final PointerIcon expectedIcon = PointerIcon.getSystemIcon(activity, PointerIcon.TYPE_HAND); 391 final MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_HOVER_MOVE, 0, 0, 0); 392 final Menu menu = mBottomNavigation.getMenu(); 393 for (int i = 0; i < menu.size(); i++) { 394 final MenuItem item = menu.getItem(i); 395 assertTrue(item.isEnabled()); 396 final View itemView = activity.findViewById(item.getItemId()); 397 assertEquals(expectedIcon, itemView.onResolvePointerIcon(event, 0)); 398 item.setEnabled(false); 399 assertEquals(null, itemView.onResolvePointerIcon(event, 0)); 400 item.setEnabled(true); 401 assertEquals(expectedIcon, itemView.onResolvePointerIcon(event, 0)); 402 } 403 } 404 405 @UiThreadTest 406 @Test 407 @SmallTest 408 public void testClearingMenu() throws Throwable { 409 mBottomNavigation.getMenu().clear(); 410 assertEquals(0, mBottomNavigation.getMenu().size()); 411 mBottomNavigation.inflateMenu(R.menu.bottom_navigation_view_content); 412 assertEquals(3, mBottomNavigation.getMenu().size()); 413 } 414 415 @Test 416 @SmallTest 417 public void testSavedState() throws Throwable { 418 // Select an item other than the first 419 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 420 isDescendantOfA(withId(R.id.bottom_navigation)), isDisplayed())).perform(click()); 421 assertTrue(mBottomNavigation.getMenu().findItem(R.id.destination_profile).isChecked()); 422 // Save the state 423 final Parcelable state = mBottomNavigation.onSaveInstanceState(); 424 425 // Restore the state into a fresh BottomNavigationView 426 mActivityTestRule.runOnUiThread(new Runnable() { 427 @Override 428 public void run() { 429 BottomNavigationView testView = 430 new BottomNavigationView(mActivityTestRule.getActivity()); 431 testView.inflateMenu(R.menu.bottom_navigation_view_content); 432 testView.onRestoreInstanceState(state); 433 assertTrue(testView.getMenu().findItem(R.id.destination_profile).isChecked()); 434 } 435 }); 436 } 437 438 @UiThreadTest 439 @Test 440 @SmallTest 441 public void testContentDescription() { 442 ViewGroup menuView = (ViewGroup) mBottomNavigation.getChildAt(0); 443 final int count = menuView.getChildCount(); 444 for (int i = 0; i < count; i++) { 445 View child = menuView.getChildAt(i); 446 // We're using the same strings for content description 447 assertEquals(mMenuStringContent.get(child.getId()), 448 child.getContentDescription().toString()); 449 } 450 451 menuView.getChildAt(0).getContentDescription(); 452 } 453 454 private void checkAndVerifyExclusiveItem(final Menu menu, final int id) throws Throwable { 455 menu.findItem(id).setChecked(true); 456 for (int i = 0; i < menu.size(); i++) { 457 final MenuItem item = menu.getItem(i); 458 if (item.getItemId() == id) { 459 assertTrue(item.isChecked()); 460 } else { 461 assertFalse(item.isChecked()); 462 } 463 } 464 } 465} 466