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