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.DrawerLayoutActions.closeDrawer; 19import static android.support.design.testutils.DrawerLayoutActions.openDrawer; 20import static android.support.design.testutils.NavigationViewActions.addHeaderView; 21import static android.support.design.testutils.NavigationViewActions.inflateHeaderView; 22import static android.support.design.testutils.NavigationViewActions.removeHeaderView; 23import static android.support.design.testutils.NavigationViewActions.removeMenuItem; 24import static android.support.design.testutils.NavigationViewActions.setCheckedItem; 25import static android.support.design.testutils.NavigationViewActions.setIconForMenuItem; 26import static android.support.design.testutils.NavigationViewActions.setItemBackground; 27import static android.support.design.testutils.NavigationViewActions.setItemBackgroundResource; 28import static android.support.design.testutils.NavigationViewActions.setItemIconTintList; 29import static android.support.design.testutils.NavigationViewActions.setItemTextAppearance; 30import static android.support.design.testutils.NavigationViewActions.setItemTextColor; 31import static android.support.design.testutils.TestUtilsActions.reinflateMenu; 32import static android.support.design.testutils.TestUtilsActions.restoreHierarchyState; 33import static android.support.design.testutils.TestUtilsMatchers.isActionViewOf; 34import static android.support.design.testutils.TestUtilsMatchers.isChildOfA; 35import static android.support.design.testutils.TestUtilsMatchers.withBackgroundFill; 36import static android.support.design.testutils.TestUtilsMatchers.withStartDrawableFilledWith; 37import static android.support.design.testutils.TestUtilsMatchers.withTextColor; 38import static android.support.design.testutils.TestUtilsMatchers.withTextSize; 39import static android.support.test.espresso.Espresso.onView; 40import static android.support.test.espresso.action.ViewActions.click; 41import static android.support.test.espresso.assertion.ViewAssertions.matches; 42import static android.support.test.espresso.matcher.ViewMatchers.Visibility; 43import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; 44import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; 45import static android.support.test.espresso.matcher.ViewMatchers.isChecked; 46import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; 47import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; 48import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 49import static android.support.test.espresso.matcher.ViewMatchers.isNotChecked; 50import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; 51import static android.support.test.espresso.matcher.ViewMatchers.withId; 52import static android.support.test.espresso.matcher.ViewMatchers.withText; 53 54import static org.hamcrest.core.AllOf.allOf; 55import static org.junit.Assert.assertEquals; 56import static org.junit.Assert.assertFalse; 57import static org.junit.Assert.assertNotNull; 58import static org.junit.Assert.assertTrue; 59import static org.mockito.Mockito.mock; 60import static org.mockito.Mockito.times; 61import static org.mockito.Mockito.verify; 62import static org.mockito.Mockito.verifyNoMoreInteractions; 63 64import android.annotation.TargetApi; 65import android.content.res.Resources; 66import android.os.Build; 67import android.os.Parcelable; 68import android.support.annotation.ColorInt; 69import android.support.annotation.IdRes; 70import android.support.design.test.R; 71import android.support.design.testutils.TestDrawable; 72import android.support.test.filters.LargeTest; 73import android.support.test.filters.SdkSuppress; 74import android.support.v4.content.res.ResourcesCompat; 75import android.support.v4.view.GravityCompat; 76import android.support.v4.widget.DrawerLayout; 77import android.support.v7.widget.RecyclerView; 78import android.support.v7.widget.SwitchCompat; 79import android.util.SparseArray; 80import android.view.LayoutInflater; 81import android.view.Menu; 82import android.view.MenuItem; 83import android.view.View; 84import android.widget.TextView; 85 86import org.hamcrest.Matcher; 87import org.junit.Before; 88import org.junit.Test; 89 90import java.util.HashMap; 91import java.util.Map; 92 93@LargeTest 94public class NavigationViewTest 95 extends BaseInstrumentationTestCase<NavigationViewActivity> { 96 private static final int[] MENU_CONTENT_ITEM_IDS = { R.id.destination_home, 97 R.id.destination_profile, R.id.destination_people, R.id.destination_settings }; 98 private Map<Integer, String> mMenuStringContent; 99 100 private DrawerLayout mDrawerLayout; 101 102 private NavigationTestView mNavigationView; 103 104 public NavigationViewTest() { 105 super(NavigationViewActivity.class); 106 } 107 108 @Before 109 public void setUp() throws Exception { 110 final NavigationViewActivity activity = mActivityTestRule.getActivity(); 111 mDrawerLayout = (DrawerLayout) activity.findViewById(R.id.drawer_layout); 112 mNavigationView = (NavigationTestView) mDrawerLayout.findViewById(R.id.start_drawer); 113 114 // Close the drawer to reset the state for the next test 115 onView(withId(R.id.drawer_layout)).perform(closeDrawer(GravityCompat.START)); 116 117 final Resources res = activity.getResources(); 118 mMenuStringContent = new HashMap<>(MENU_CONTENT_ITEM_IDS.length); 119 mMenuStringContent.put(R.id.destination_home, res.getString(R.string.navigate_home)); 120 mMenuStringContent.put(R.id.destination_profile, res.getString(R.string.navigate_profile)); 121 mMenuStringContent.put(R.id.destination_people, res.getString(R.string.navigate_people)); 122 mMenuStringContent.put(R.id.destination_settings, 123 res.getString(R.string.navigate_settings)); 124 } 125 126 @Test 127 public void testBasics() { 128 // Open our drawer 129 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 130 131 // Check the contents of the Menu object 132 final Menu menu = mNavigationView.getMenu(); 133 assertNotNull("Menu should not be null", menu); 134 assertEquals("Should have matching number of items", MENU_CONTENT_ITEM_IDS.length + 1, 135 menu.size()); 136 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 137 final MenuItem currItem = menu.getItem(i); 138 assertEquals("ID for Item #" + i, MENU_CONTENT_ITEM_IDS[i], currItem.getItemId()); 139 } 140 141 // Check that we have the expected menu items in our NavigationView 142 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 143 onView(allOf(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i])), 144 isDescendantOfA(withId(R.id.start_drawer)))).check(matches(isDisplayed())); 145 } 146 } 147 148 @Test 149 public void testWillNotDraw() { 150 // Open our drawer 151 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 152 153 if (Build.VERSION.SDK_INT >= 21) { 154 if (mNavigationView.hasSystemWindowInsets()) { 155 assertFalse(mNavigationView.willNotDraw()); 156 } else { 157 assertTrue(mNavigationView.willNotDraw()); 158 } 159 } else { 160 assertTrue(mNavigationView.willNotDraw()); 161 } 162 } 163 164 @Test 165 public void testTextAppearance() { 166 // Open our drawer 167 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 168 169 final Resources res = mActivityTestRule.getActivity().getResources(); 170 final int defaultTextSize = res.getDimensionPixelSize(R.dimen.text_medium_size); 171 172 // Check the default style of the menu items in our NavigationView 173 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 174 onView(allOf(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i])), 175 isDescendantOfA(withId(R.id.start_drawer)))).check( 176 matches(withTextSize(defaultTextSize))); 177 } 178 179 // Set a new text appearance on our NavigationView 180 onView(withId(R.id.start_drawer)).perform(setItemTextAppearance(R.style.TextSmallStyle)); 181 182 // And check that all the menu items have the new style 183 final int newTextSize = res.getDimensionPixelSize(R.dimen.text_small_size); 184 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 185 onView(allOf(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i])), 186 isDescendantOfA(withId(R.id.start_drawer)))).check( 187 matches(withTextSize(newTextSize))); 188 } 189 } 190 191 @Test 192 public void testTextColor() { 193 // Open our drawer 194 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 195 196 final Resources res = mActivityTestRule.getActivity().getResources(); 197 final @ColorInt int defaultTextColor = ResourcesCompat.getColor(res, 198 R.color.emerald_text, null); 199 200 // Check the default text color of the menu items in our NavigationView 201 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 202 onView(allOf(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i])), 203 isDescendantOfA(withId(R.id.start_drawer)))).check( 204 matches(withTextColor(defaultTextColor))); 205 } 206 207 // Set a new text color on our NavigationView 208 onView(withId(R.id.start_drawer)).perform(setItemTextColor( 209 ResourcesCompat.getColorStateList(res, R.color.color_state_list_lilac, null))); 210 211 // And check that all the menu items have the new color 212 final @ColorInt int newTextColor = ResourcesCompat.getColor(res, 213 R.color.lilac_default, null); 214 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 215 onView(allOf(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i])), 216 isDescendantOfA(withId(R.id.start_drawer)))).check( 217 matches(withTextColor(newTextColor))); 218 } 219 } 220 221 @Test 222 public void testBackground() { 223 // Open our drawer 224 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 225 226 final Resources res = mActivityTestRule.getActivity().getResources(); 227 final @ColorInt int defaultFillColor = ResourcesCompat.getColor(res, 228 R.color.sand_default, null); 229 230 // Check the default fill color of the menu items in our NavigationView 231 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 232 // Note that here we're tying ourselves to the implementation details of the 233 // internal structure of the NavigationView. Specifically, we're looking at the 234 // direct child of RecyclerView which is expected to have the background set 235 // on it. If the internal implementation of NavigationView changes, the second 236 // Matcher below will need to be tweaked. 237 Matcher menuItemMatcher = allOf( 238 hasDescendant(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i]))), 239 isChildOfA(isAssignableFrom(RecyclerView.class)), 240 isDescendantOfA(withId(R.id.start_drawer))); 241 242 onView(menuItemMatcher).check(matches(withBackgroundFill(defaultFillColor))); 243 } 244 245 // Set a new background (flat fill color) on our NavigationView 246 onView(withId(R.id.start_drawer)).perform(setItemBackgroundResource( 247 R.drawable.test_background_blue)); 248 249 // And check that all the menu items have the new fill 250 final @ColorInt int newFillColorBlue = ResourcesCompat.getColor(res, 251 R.color.test_blue, null); 252 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 253 Matcher menuItemMatcher = allOf( 254 hasDescendant(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i]))), 255 isChildOfA(isAssignableFrom(RecyclerView.class)), 256 isDescendantOfA(withId(R.id.start_drawer))); 257 258 onView(menuItemMatcher).check(matches(withBackgroundFill(newFillColorBlue))); 259 } 260 261 // Set another new background on our NavigationView 262 onView(withId(R.id.start_drawer)).perform(setItemBackground( 263 ResourcesCompat.getDrawable(res, R.drawable.test_background_green, null))); 264 265 // And check that all the menu items have the new fill 266 final @ColorInt int newFillColorGreen = ResourcesCompat.getColor(res, 267 R.color.test_green, null); 268 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 269 Matcher menuItemMatcher = allOf( 270 hasDescendant(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i]))), 271 isChildOfA(isAssignableFrom(RecyclerView.class)), 272 isDescendantOfA(withId(R.id.start_drawer))); 273 274 onView(menuItemMatcher).check(matches(withBackgroundFill(newFillColorGreen))); 275 } 276 } 277 278 @Test 279 public void testIconTinting() { 280 // Open our drawer 281 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 282 283 final Resources res = mActivityTestRule.getActivity().getResources(); 284 final @ColorInt int redFill = ResourcesCompat.getColor(res, R.color.test_red, null); 285 final @ColorInt int greenFill = ResourcesCompat.getColor(res, R.color.test_green, null); 286 final @ColorInt int blueFill = ResourcesCompat.getColor(res, R.color.test_blue, null); 287 final int iconSize = res.getDimensionPixelSize(R.dimen.drawable_small_size); 288 onView(withId(R.id.start_drawer)).perform(setIconForMenuItem(R.id.destination_home, 289 new TestDrawable(redFill, iconSize, iconSize))); 290 onView(withId(R.id.start_drawer)).perform(setIconForMenuItem(R.id.destination_profile, 291 new TestDrawable(greenFill, iconSize, iconSize))); 292 onView(withId(R.id.start_drawer)).perform(setIconForMenuItem(R.id.destination_people, 293 new TestDrawable(blueFill, iconSize, iconSize))); 294 295 final @ColorInt int defaultTintColor = ResourcesCompat.getColor(res, 296 R.color.emerald_translucent, null); 297 298 // We're allowing a margin of error in checking the color of the items' icons. 299 // This is due to the translucent color being used in the icon tinting 300 // and off-by-one discrepancies of SRC_IN when it's compositing 301 // translucent color. Note that all the checks below are written for the current 302 // logic on NavigationView that uses the default SRC_IN tint mode - effectively 303 // replacing all non-transparent pixels in the destination (original icon) with 304 // our translucent tint color. 305 final int allowedComponentVariance = 1; 306 307 // Note that here we're tying ourselves to the implementation details of the 308 // internal structure of the NavigationView. Specifically, we're checking the 309 // start drawable of the text view with the specific text. If the internal 310 // implementation of NavigationView changes, the second Matcher in the lookups 311 // below will need to be tweaked. 312 onView(allOf(withText(mMenuStringContent.get(R.id.destination_home)), 313 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 314 withStartDrawableFilledWith(defaultTintColor, allowedComponentVariance))); 315 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 316 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 317 withStartDrawableFilledWith(defaultTintColor, allowedComponentVariance))); 318 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 319 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 320 withStartDrawableFilledWith(defaultTintColor, allowedComponentVariance))); 321 322 final @ColorInt int newTintColor = ResourcesCompat.getColor(res, 323 R.color.red_translucent, null); 324 325 onView(withId(R.id.start_drawer)).perform(setItemIconTintList( 326 ResourcesCompat.getColorStateList(res, R.color.color_state_list_red_translucent, 327 null))); 328 // Check that all menu items with icons now have icons tinted with the newly set color 329 onView(allOf(withText(mMenuStringContent.get(R.id.destination_home)), 330 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 331 withStartDrawableFilledWith(newTintColor, allowedComponentVariance))); 332 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 333 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 334 withStartDrawableFilledWith(newTintColor, allowedComponentVariance))); 335 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 336 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 337 withStartDrawableFilledWith(newTintColor, allowedComponentVariance))); 338 339 // And now remove all icon tinting 340 onView(withId(R.id.start_drawer)).perform(setItemIconTintList(null)); 341 // And verify that all menu items with icons now have the original colors for their icons. 342 // Note that since there is no tinting at this point, we don't allow any color variance 343 // in these checks. 344 onView(allOf(withText(mMenuStringContent.get(R.id.destination_home)), 345 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 346 withStartDrawableFilledWith(redFill, 0))); 347 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 348 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 349 withStartDrawableFilledWith(greenFill, 0))); 350 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 351 isDescendantOfA(withId(R.id.start_drawer)))).check(matches( 352 withStartDrawableFilledWith(blueFill, 0))); 353 } 354 355 /** 356 * Gets the list of header IDs (which can be empty) and verifies that the actual header content 357 * of our navigation view matches the expected header content. 358 */ 359 private void verifyHeaders(@IdRes int ... expectedHeaderIds) { 360 final int expectedHeaderCount = (expectedHeaderIds != null) ? expectedHeaderIds.length : 0; 361 final int actualHeaderCount = mNavigationView.getHeaderCount(); 362 assertEquals("Header count", expectedHeaderCount, actualHeaderCount); 363 364 if (expectedHeaderCount > 0) { 365 for (int i = 0; i < expectedHeaderCount; i++) { 366 final View currentHeader = mNavigationView.getHeaderView(i); 367 assertEquals("Header at #" + i, expectedHeaderIds[i], currentHeader.getId()); 368 } 369 } 370 } 371 372 @Test 373 public void testHeaders() { 374 // Open our drawer 375 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 376 377 // We should have no headers at the start 378 verifyHeaders(); 379 380 // Inflate one header and check that it's there in the navigation view 381 onView(withId(R.id.start_drawer)).perform( 382 inflateHeaderView(R.layout.design_navigation_view_header1)); 383 verifyHeaders(R.id.header1); 384 385 final LayoutInflater inflater = LayoutInflater.from(mActivityTestRule.getActivity()); 386 387 // Add one more header and check that it's there in the navigation view 388 onView(withId(R.id.start_drawer)).perform( 389 addHeaderView(inflater, R.layout.design_navigation_view_header2)); 390 verifyHeaders(R.id.header1, R.id.header2); 391 392 final View header1 = mNavigationView.findViewById(R.id.header1); 393 // Remove the first header and check that we still have the second header 394 onView(withId(R.id.start_drawer)).perform(removeHeaderView(header1)); 395 verifyHeaders(R.id.header2); 396 397 // Add one more header and check that we now have two headers 398 onView(withId(R.id.start_drawer)).perform( 399 inflateHeaderView(R.layout.design_navigation_view_header3)); 400 verifyHeaders(R.id.header2, R.id.header3); 401 402 // Add another "copy" of the header from the just-added layout and check that we now 403 // have three headers 404 onView(withId(R.id.start_drawer)).perform( 405 addHeaderView(inflater, R.layout.design_navigation_view_header3)); 406 verifyHeaders(R.id.header2, R.id.header3, R.id.header3); 407 } 408 409 @SdkSuppress(minSdkVersion = 11) 410 @TargetApi(11) 411 @Test 412 public void testHeaderState() { 413 // Open our drawer 414 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 415 416 // Inflate a header with a toggle switch and check that it's there in the navigation view 417 onView(withId(R.id.start_drawer)).perform( 418 inflateHeaderView(R.layout.design_navigation_view_header_switch)); 419 verifyHeaders(R.id.header_frame); 420 421 onView(withId(R.id.header_toggle)) 422 .check(matches(isNotChecked())) 423 .perform(click()) 424 .check(matches(isChecked())); 425 426 // Save the current state 427 SparseArray<Parcelable> container = new SparseArray<>(); 428 mNavigationView.saveHierarchyState(container); 429 430 // Remove the header 431 final View header = mNavigationView.findViewById(R.id.header_frame); 432 onView(withId(R.id.start_drawer)).perform(removeHeaderView(header)); 433 verifyHeaders(); 434 435 // Inflate the header again 436 onView(withId(R.id.start_drawer)).perform( 437 inflateHeaderView(R.layout.design_navigation_view_header_switch)); 438 verifyHeaders(R.id.header_frame); 439 440 // Restore the saved state 441 onView(withId(R.id.start_drawer)).perform( 442 restoreHierarchyState(container)); 443 444 // Confirm that the state was restored 445 onView(withId(R.id.header_toggle)) 446 .check(matches(isChecked())); 447 } 448 449 @SdkSuppress(minSdkVersion = 11) 450 @TargetApi(11) 451 @Test 452 public void testActionViewState() { 453 // Open our drawer 454 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 455 456 final Menu menu = mNavigationView.getMenu(); 457 onView(isActionViewOf(menu, R.id.destination_people)) 458 .check(matches(isNotChecked())) // Not checked by default 459 .perform(click()) // Check it 460 .check(matches(isChecked())); 461 462 // Remove the other action view to simulate the case where it is not yet inflated 463 onView(isActionViewOf(menu, R.id.destination_custom)) 464 .check(matches(isDisplayed())); 465 onView(withId(R.id.start_drawer)) 466 .perform(removeMenuItem(R.id.destination_custom)); 467 468 // Save the current state 469 SparseArray<Parcelable> container = new SparseArray<>(); 470 mNavigationView.saveHierarchyState(container); 471 472 // Restore the saved state 473 onView(withId(R.id.start_drawer)) 474 .perform(reinflateMenu(R.menu.navigation_view_content)) 475 .perform(restoreHierarchyState(container)); 476 477 // Checked state should be restored 478 onView(isActionViewOf(menu, R.id.destination_people)) 479 .check(matches(isChecked())); 480 } 481 482 @Test 483 public void testNavigationSelectionListener() { 484 // Open our drawer 485 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 486 487 // Click one of our items 488 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 489 isDescendantOfA(withId(R.id.start_drawer)))).perform(click()); 490 // Check that the drawer is still open 491 assertTrue("Drawer is still open after click", 492 mDrawerLayout.isDrawerOpen(GravityCompat.START)); 493 494 // Register a listener 495 NavigationView.OnNavigationItemSelectedListener mockedListener = 496 mock(NavigationView.OnNavigationItemSelectedListener.class); 497 mNavigationView.setNavigationItemSelectedListener(mockedListener); 498 499 // Click one of our items 500 onView(allOf(withText(mMenuStringContent.get(R.id.destination_profile)), 501 isDescendantOfA(withId(R.id.start_drawer)))).perform(click()); 502 // Check that the drawer is still open 503 assertTrue("Drawer is still open after click", 504 mDrawerLayout.isDrawerOpen(GravityCompat.START)); 505 // And that our listener has been notified of the click 506 verify(mockedListener, times(1)).onNavigationItemSelected( 507 mNavigationView.getMenu().findItem(R.id.destination_profile)); 508 509 // Set null listener to test that the next click is not going to notify the 510 // previously set listener 511 mNavigationView.setNavigationItemSelectedListener(null); 512 513 // Click one of our items 514 onView(allOf(withText(mMenuStringContent.get(R.id.destination_settings)), 515 isDescendantOfA(withId(R.id.start_drawer)))).perform(click()); 516 // Check that the drawer is still open 517 assertTrue("Drawer is still open after click", 518 mDrawerLayout.isDrawerOpen(GravityCompat.START)); 519 // And that our previous listener has not been notified of the click 520 verifyNoMoreInteractions(mockedListener); 521 } 522 523 private void verifyCheckedAppearance(@IdRes int checkedItemId, 524 @ColorInt int uncheckedItemForeground, @ColorInt int checkedItemForeground, 525 @ColorInt int uncheckedItemBackground, @ColorInt int checkedItemBackground) { 526 for (int i = 0; i < MENU_CONTENT_ITEM_IDS.length; i++) { 527 final boolean expectedToBeChecked = (MENU_CONTENT_ITEM_IDS[i] == checkedItemId); 528 final @ColorInt int expectedItemForeground = 529 expectedToBeChecked ? checkedItemForeground : uncheckedItemForeground; 530 final @ColorInt int expectedItemBackground = 531 expectedToBeChecked ? checkedItemBackground : uncheckedItemBackground; 532 533 // For the background fill check we need to select a view that has its background 534 // set by the current implementation (see disclaimer in testBackground) 535 Matcher menuItemMatcher = allOf( 536 hasDescendant(withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i]))), 537 isChildOfA(isAssignableFrom(RecyclerView.class)), 538 isDescendantOfA(withId(R.id.start_drawer))); 539 onView(menuItemMatcher).check(matches(withBackgroundFill(expectedItemBackground))); 540 541 // And for the foreground color check we need to select a view with the text content 542 Matcher menuItemTextMatcher = allOf( 543 withText(mMenuStringContent.get(MENU_CONTENT_ITEM_IDS[i])), 544 isDescendantOfA(withId(R.id.start_drawer))); 545 onView(menuItemTextMatcher).check(matches(withTextColor(expectedItemForeground))); 546 } 547 } 548 549 @Test 550 public void testCheckedAppearance() { 551 // Open our drawer 552 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 553 554 // Reconfigure our navigation view to use foreground (text) and background visuals 555 // with explicitly different colors for the checked state 556 final Resources res = mActivityTestRule.getActivity().getResources(); 557 onView(withId(R.id.start_drawer)).perform(setItemTextColor( 558 ResourcesCompat.getColorStateList(res, R.color.color_state_list_sand, null))); 559 onView(withId(R.id.start_drawer)).perform(setItemBackgroundResource( 560 R.drawable.test_drawable_state_list)); 561 562 final @ColorInt int uncheckedItemForeground = ResourcesCompat.getColor(res, 563 R.color.sand_default, null); 564 final @ColorInt int checkedItemForeground = ResourcesCompat.getColor(res, 565 R.color.sand_checked, null); 566 final @ColorInt int uncheckedItemBackground = ResourcesCompat.getColor(res, 567 R.color.test_green, null); 568 final @ColorInt int checkedItemBackground = ResourcesCompat.getColor(res, 569 R.color.test_blue, null); 570 571 // Verify that all items are rendered with unchecked visuals 572 verifyCheckedAppearance(0, uncheckedItemForeground, checkedItemForeground, 573 uncheckedItemBackground, checkedItemBackground); 574 575 // Mark one of the items as checked 576 onView(withId(R.id.start_drawer)).perform(setCheckedItem(R.id.destination_profile)); 577 // And verify that it's now rendered with checked visuals 578 verifyCheckedAppearance(R.id.destination_profile, 579 uncheckedItemForeground, checkedItemForeground, 580 uncheckedItemBackground, checkedItemBackground); 581 582 // Register a navigation listener that "marks" the selected item 583 mNavigationView.setNavigationItemSelectedListener( 584 new NavigationView.OnNavigationItemSelectedListener() { 585 @Override 586 public boolean onNavigationItemSelected(MenuItem item) { 587 return true; 588 } 589 }); 590 591 // Click one of our items 592 onView(allOf(withText(mMenuStringContent.get(R.id.destination_people)), 593 isDescendantOfA(withId(R.id.start_drawer)))).perform(click()); 594 // and verify that it's now checked 595 verifyCheckedAppearance(R.id.destination_people, 596 uncheckedItemForeground, checkedItemForeground, 597 uncheckedItemBackground, checkedItemBackground); 598 599 // Register a navigation listener that doesn't "mark" the selected item 600 mNavigationView.setNavigationItemSelectedListener( 601 new NavigationView.OnNavigationItemSelectedListener() { 602 @Override 603 public boolean onNavigationItemSelected(MenuItem item) { 604 return false; 605 } 606 }); 607 608 // Click another items 609 onView(allOf(withText(mMenuStringContent.get(R.id.destination_settings)), 610 isDescendantOfA(withId(R.id.start_drawer)))).perform(click()); 611 // and verify that the checked state remains on the previously clicked item 612 // since the current navigation listener returns false from its callback 613 // implementation 614 verifyCheckedAppearance(R.id.destination_people, 615 uncheckedItemForeground, checkedItemForeground, 616 uncheckedItemBackground, checkedItemBackground); 617 } 618 619 @Test 620 public void testActionLayout() { 621 // Open our drawer 622 onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START)); 623 624 // There are four conditions to "find" the menu item with action layout (switch): 625 // 1. Is in the NavigationView 626 // 2. Is direct child of a class that extends RecyclerView 627 // 3. Has a child with "people" text 628 // 4. Has fully displayed child that extends SwitchCompat 629 // Note that condition 2 makes a certain assumption about the internal implementation 630 // details of the NavigationMenu, while conditions 3 and 4 aim to be as generic as 631 // possible and to not rely on the internal details of the current layout implementation 632 // of an individual menu item in NavigationMenu. 633 Matcher menuItemMatcher = allOf( 634 isDescendantOfA(withId(R.id.start_drawer)), 635 isChildOfA(isAssignableFrom(RecyclerView.class)), 636 hasDescendant(withText(mMenuStringContent.get(R.id.destination_people))), 637 hasDescendant(allOf( 638 isAssignableFrom(SwitchCompat.class), 639 isCompletelyDisplayed()))); 640 641 // While we don't need to perform any action on our row, the invocation of perform() 642 // makes our matcher actually run. If for some reason NavigationView fails to inflate and 643 // display our SwitchCompat action layout, the next line will fail in the matcher pass. 644 onView(menuItemMatcher).perform(click()); 645 646 // Check that the full custom view is displayed without title and icon. 647 final Resources res = mActivityTestRule.getActivity().getResources(); 648 Matcher customItemMatcher = allOf( 649 isDescendantOfA(withId(R.id.start_drawer)), 650 isChildOfA(isAssignableFrom(RecyclerView.class)), 651 hasDescendant(withText(res.getString(R.string.navigate_custom))), 652 hasDescendant(allOf( 653 isAssignableFrom(TextView.class), 654 withEffectiveVisibility(Visibility.GONE)))); 655 onView(customItemMatcher).perform(click()); 656 } 657} 658