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