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