• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  */
16 package android.support.design.widget;
17 
18 import static android.support.design.testutils.DrawerLayoutActions.closeDrawer;
19 import static android.support.design.testutils.DrawerLayoutActions.openDrawer;
20 import static android.support.design.testutils.NavigationViewActions.addHeaderView;
21 import static android.support.design.testutils.NavigationViewActions.inflateHeaderView;
22 import static android.support.design.testutils.NavigationViewActions.removeHeaderView;
23 import static android.support.design.testutils.NavigationViewActions.removeMenuItem;
24 import static android.support.design.testutils.NavigationViewActions.setCheckedItem;
25 import static android.support.design.testutils.NavigationViewActions.setIconForMenuItem;
26 import static android.support.design.testutils.NavigationViewActions.setItemBackground;
27 import static android.support.design.testutils.NavigationViewActions.setItemBackgroundResource;
28 import static android.support.design.testutils.NavigationViewActions.setItemIconTintList;
29 import static android.support.design.testutils.NavigationViewActions.setItemTextAppearance;
30 import static android.support.design.testutils.NavigationViewActions.setItemTextColor;
31 import static android.support.design.testutils.TestUtilsActions.reinflateMenu;
32 import static android.support.design.testutils.TestUtilsActions.restoreHierarchyState;
33 import static android.support.design.testutils.TestUtilsMatchers.isActionViewOf;
34 import static android.support.design.testutils.TestUtilsMatchers.isChildOfA;
35 import static android.support.design.testutils.TestUtilsMatchers.withBackgroundFill;
36 import static android.support.design.testutils.TestUtilsMatchers.withStartDrawableFilledWith;
37 import static android.support.design.testutils.TestUtilsMatchers.withTextColor;
38 import static android.support.design.testutils.TestUtilsMatchers.withTextSize;
39 import static android.support.test.espresso.Espresso.onView;
40 import static android.support.test.espresso.action.ViewActions.click;
41 import static android.support.test.espresso.assertion.ViewAssertions.matches;
42 import static android.support.test.espresso.matcher.ViewMatchers.Visibility;
43 import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
44 import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
45 import static android.support.test.espresso.matcher.ViewMatchers.isChecked;
46 import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
47 import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
48 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
49 import static android.support.test.espresso.matcher.ViewMatchers.isNotChecked;
50 import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
51 import static android.support.test.espresso.matcher.ViewMatchers.withId;
52 import static android.support.test.espresso.matcher.ViewMatchers.withText;
53 
54 import static org.hamcrest.core.AllOf.allOf;
55 import static org.junit.Assert.assertEquals;
56 import static org.junit.Assert.assertFalse;
57 import static org.junit.Assert.assertNotNull;
58 import static org.junit.Assert.assertTrue;
59 import static org.mockito.Mockito.mock;
60 import static org.mockito.Mockito.times;
61 import static org.mockito.Mockito.verify;
62 import static org.mockito.Mockito.verifyNoMoreInteractions;
63 
64 import android.annotation.TargetApi;
65 import android.content.res.Resources;
66 import android.os.Build;
67 import android.os.Parcelable;
68 import android.support.annotation.ColorInt;
69 import android.support.annotation.IdRes;
70 import android.support.design.test.R;
71 import android.support.design.testutils.TestDrawable;
72 import android.support.test.filters.MediumTest;
73 import android.support.test.filters.SdkSuppress;
74 import android.support.v4.content.res.ResourcesCompat;
75 import android.support.v4.view.GravityCompat;
76 import android.support.v4.widget.DrawerLayout;
77 import android.support.v7.widget.RecyclerView;
78 import android.support.v7.widget.SwitchCompat;
79 import android.util.SparseArray;
80 import android.view.LayoutInflater;
81 import android.view.Menu;
82 import android.view.MenuItem;
83 import android.view.View;
84 import android.widget.TextView;
85 
86 import org.hamcrest.Matcher;
87 import org.junit.Before;
88 import org.junit.Test;
89 
90 import java.util.HashMap;
91 import java.util.Map;
92 
93 @MediumTest
94 public 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 
NavigationViewTest()104     public NavigationViewTest() {
105         super(NavigationViewActivity.class);
106     }
107 
108     @Before
setUp()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
testBasics()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
testWillNotDraw()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
testTextAppearance()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
testTextColor()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
testBackground()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
testIconTinting()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      */
verifyHeaders(@dRes int ... expectedHeaderIds)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
testHeaders()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
testHeaderState()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
testActionViewState()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
testNavigationSelectionListener()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 
verifyCheckedAppearance(@dRes int checkedItemId, @ColorInt int uncheckedItemForeground, @ColorInt int checkedItemForeground, @ColorInt int uncheckedItemBackground, @ColorInt int checkedItemBackground)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
testCheckedAppearance()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
testActionLayout()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