chromium/chrome/android/javatests/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettingsSearchTest.java

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.password_manager.settings;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;

import static org.chromium.chrome.browser.password_manager.settings.PasswordSettingsTestHelper.ARES_AT_OLYMP;
import static org.chromium.chrome.browser.password_manager.settings.PasswordSettingsTestHelper.DEIMOS_AT_OLYMP;
import static org.chromium.chrome.browser.password_manager.settings.PasswordSettingsTestHelper.GREEK_GODS;
import static org.chromium.chrome.browser.password_manager.settings.PasswordSettingsTestHelper.HADES_AT_UNDERWORLD;
import static org.chromium.chrome.browser.password_manager.settings.PasswordSettingsTestHelper.PHOBOS_AT_OLYMP;
import static org.chromium.chrome.browser.password_manager.settings.PasswordSettingsTestHelper.ZEUS_ON_EARTH;
import static org.chromium.ui.test.util.ViewUtils.VIEW_GONE;
import static org.chromium.ui.test.util.ViewUtils.VIEW_INVISIBLE;
import static org.chromium.ui.test.util.ViewUtils.VIEW_NULL;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.app.Instrumentation;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;

import androidx.annotation.IdRes;
import androidx.annotation.StringRes;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.test.espresso.Espresso;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseActivityTestRule;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.RequiresRestart;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.history.HistoryActivity;
import org.chromium.chrome.browser.history.HistoryContentManager;
import org.chromium.chrome.browser.history.StubbedHistoryProvider;
import org.chromium.chrome.browser.password_check.PasswordCheck;
import org.chromium.chrome.browser.password_check.PasswordCheckFactory;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.ui.test.util.ViewUtils;

import java.util.Date;
import java.util.concurrent.atomic.AtomicReference;

/** Tests for the search feature of the "Passwords" settings screen. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class PasswordSettingsSearchTest {
    private static final long UI_UPDATING_TIMEOUT_MS = 3000;

    @Rule
    public BaseActivityTestRule<HistoryActivity> mHistoryActivityTestRule =
            new BaseActivityTestRule<>(HistoryActivity.class);

    @Rule
    public SettingsActivityTestRule<PasswordSettings> mSettingsActivityTestRule =
            new SettingsActivityTestRule<>(PasswordSettings.class);

    @Mock private PasswordCheck mPasswordCheck;

    private final PasswordSettingsTestHelper mTestHelper = new PasswordSettingsTestHelper();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        PasswordCheckFactory.setPasswordCheckForTesting(mPasswordCheck);
    }

    @After
    public void tearDown() {
        mTestHelper.tearDown();
    }

    /** Check that the search item is visible in the action bar. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    @SuppressWarnings("AlwaysShowAction") // We need to ensure the icon is in the action bar.
    public void testSearchIconVisibleInActionBar() {
        mTestHelper.setPasswordSource(null); // Initialize empty preferences.
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);
        onViewWaiting(withText(R.string.password_manager_settings_title));
        PasswordSettings f = mSettingsActivityTestRule.getFragment();

        // Force the search option into the action bar.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    f.getMenuForTesting()
                            .findItem(R.id.menu_id_search)
                            .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
                });

        onView(withId(R.id.menu_id_search)).check(matches(isDisplayed()));
    }

    /** Check that the search item is visible in the overflow menu. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchTextInOverflowMenuVisible() {
        mTestHelper.setPasswordSource(
                null); // Initialize empty preferences.mSettingsActivityTestRule
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);
        onViewWaiting(withText(R.string.password_manager_settings_title));
        PasswordSettings f = mSettingsActivityTestRule.getFragment();

        // Force the search option into the overflow menu.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    f.getMenuForTesting()
                            .findItem(R.id.menu_id_search)
                            .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
                });

        // Open the overflow menu.
        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());

        onView(withText(R.string.search)).check(matches(isDisplayed()));
    }

    /**
     * Check that searching doesn't push the help icon into the overflow menu permanently. On screen
     * sizes where the help item starts out in the overflow menu, ensure it stays there.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testTriggeringSearchRestoresHelpIcon() {
        mTestHelper.setPasswordSource(null);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);
        onViewWaiting(withText(R.string.password_manager_settings_title));

        // Retrieve the initial status and ensure that the help option is there at all.
        final AtomicReference<Boolean> helpInOverflowMenu = new AtomicReference<>(false);
        onView(withId(R.id.menu_id_targeted_help))
                .check(
                        (helpMenuItem, e) -> {
                            ActionMenuItemView view = (ActionMenuItemView) helpMenuItem;
                            helpInOverflowMenu.set(view == null || !view.showsIcon());
                        });
        if (helpInOverflowMenu.get()) {
            openActionBarOverflowOrOptionsMenu(
                    InstrumentationRegistry.getInstrumentation().getTargetContext());
            onView(withText(R.string.menu_help)).check(matches(isDisplayed()));
            Espresso.pressBack(); // to close the Overflow menu.
        } else {
            onView(withId(R.id.menu_id_targeted_help)).check(matches(isDisplayed()));
        }

        // Trigger the search, close it and wait for UI to be restored.
        onView(withSearchMenuIdOrText()).perform(click());
        onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
        onViewWaiting(withText(R.string.password_manager_settings_title));

        // Check that the help option is exactly where it was to begin with.
        if (helpInOverflowMenu.get()) {
            openActionBarOverflowOrOptionsMenu(
                    InstrumentationRegistry.getInstrumentation().getTargetContext());
            onView(withText(R.string.menu_help)).check(matches(isDisplayed()));
            onView(withId(R.id.menu_id_targeted_help)).check(doesNotExist());
        } else {
            onView(withText(R.string.menu_help)).check(doesNotExist());
            onView(withId(R.id.menu_id_targeted_help)).check(matches(isDisplayed()));
        }
    }

    /** Check that the search filters the list by name. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchFiltersByUserName() {
        mTestHelper.setPasswordSourceWithMultipleEntries(GREEK_GODS);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        // Search for a string matching multiple user names. Case doesn't need to match.
        onView(withSearchMenuIdOrText()).perform(click());
        onView(withId(R.id.search_src_text))
                .perform(click(), typeText("aREs"), closeSoftKeyboard());

        onView(withText(ARES_AT_OLYMP.getUserName())).check(matches(isDisplayed()));
        onView(withText(PHOBOS_AT_OLYMP.getUserName())).check(matches(isDisplayed()));
        onView(withText(DEIMOS_AT_OLYMP.getUserName())).check(matches(isDisplayed()));
        onView(withText(ZEUS_ON_EARTH.getUserName())).check(doesNotExist());
        onView(withText(HADES_AT_UNDERWORLD.getUrl())).check(doesNotExist());
    }

    /** Check that the search filters the list by URL. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchFiltersByUrl() {
        mTestHelper.setPasswordSourceWithMultipleEntries(GREEK_GODS);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        // Search for a string that matches multiple URLs. Case doesn't need to match.
        onView(withSearchMenuIdOrText()).perform(click());
        onView(withId(R.id.search_src_text))
                .perform(click(), typeText("Olymp"), closeSoftKeyboard());

        onView(withText(ARES_AT_OLYMP.getUserName())).check(matches(isDisplayed()));
        onView(withText(PHOBOS_AT_OLYMP.getUserName())).check(matches(isDisplayed()));
        onView(withText(DEIMOS_AT_OLYMP.getUserName())).check(matches(isDisplayed()));
        onView(withText(ZEUS_ON_EARTH.getUserName())).check(doesNotExist());
        onView(withText(HADES_AT_UNDERWORLD.getUrl())).check(doesNotExist());
    }

    /** Check that the search filters the list by URL. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchDisplaysNoResultMessageIfSearchTurnsUpEmpty() {
        mTestHelper.setPasswordSourceWithMultipleEntries(GREEK_GODS);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        // Open the search which should hide the Account link.
        onView(withSearchMenuIdOrText()).perform(click());

        // Search for a string that matches nothing which should leave the results entirely blank.
        onView(withId(R.id.search_src_text))
                .perform(click(), typeText("Mars"), closeSoftKeyboard());

        for (SavedPasswordEntry god : GREEK_GODS) {
            onView(allOf(withText(god.getUserName()), withText(god.getUrl())))
                    .check(doesNotExist());
        }
        onView(withText(R.string.saved_passwords_none_text)).check(doesNotExist());
        // Check that the section header for saved passwords is not present. Do not confuse it with
        // the toolbar label which contains the same string, look for the one inside a linear
        // layout.
        onView(
                        allOf(
                                withParent(isAssignableFrom(LinearLayout.class)),
                                withText(R.string.password_manager_settings_title)))
                .check(doesNotExist());
        // Check the message for no result.
        onView(withText(R.string.password_no_result)).check(matches(isDisplayed()));
    }

    /** Check that triggering the search hides all non-password prefs. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchIconClickedHidesExceptionsTemporarily() {
        mTestHelper.setPasswordExceptions(new String[] {"http://exclu.de", "http://not-inclu.de"});
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        onView(withText(R.string.section_saved_passwords_exceptions)).check(matches(isDisplayed()));

        onView(withSearchMenuIdOrText()).perform(click());
        onView(withId(R.id.search_src_text)).perform(click(), closeSoftKeyboard());

        onView(withText(R.string.section_saved_passwords_exceptions)).check(doesNotExist());

        onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync(); // Close search view.

        onView(withText(R.string.section_saved_passwords_exceptions)).check(matches(isDisplayed()));
    }

    /** Check that triggering the search hides all non-password prefs. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchIconClickedHidesGeneralPrefs() {
        mTestHelper.setPasswordSource(ZEUS_ON_EARTH);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);
        final PasswordSettings prefs = mSettingsActivityTestRule.getFragment();
        final AtomicReference<Boolean> menuInitiallyVisible = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        menuInitiallyVisible.set(
                                prefs.getToolbarForTesting().isOverflowMenuShowing()));

        onView(withText(R.string.password_settings_save_passwords)).check(matches(isDisplayed()));

        if (menuInitiallyVisible.get()) { // Check overflow menu only on large screens that have it.
            onView(withContentDescription(R.string.abc_action_menu_overflow_description))
                    .check(matches(isDisplayed()));
        }

        onView(withSearchMenuIdOrText()).perform(click());

        onView(withText(R.string.password_settings_save_passwords)).check(doesNotExist());
        ViewUtils.waitForViewCheckingState(
                withParent(withContentDescription(R.string.abc_action_menu_overflow_description)),
                VIEW_INVISIBLE | VIEW_GONE | VIEW_NULL);

        onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        if (menuInitiallyVisible.get()) { // If the overflow menu was there, it should be restored.
            onView(withContentDescription(R.string.abc_action_menu_overflow_description))
                    .check(matches(isDisplayed()));
        }
    }

    /** Check that closing the search via back button brings back all non-password prefs. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchBarBackButtonRestoresGeneralPrefs() {
        mTestHelper.setPasswordSourceWithMultipleEntries(GREEK_GODS);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        onView(withSearchMenuIdOrText()).perform(click());
        onView(withId(R.id.search_src_text)).perform(click(), typeText("Zeu"), closeSoftKeyboard());

        onView(withText(R.string.password_settings_save_passwords)).check(doesNotExist());

        onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        onView(withText(R.string.password_settings_save_passwords)).check(matches(isDisplayed()));

        onView(withId(R.id.menu_id_search)).check(matches(isDisplayed()));
    }

    /** Check that clearing the search also hides the clear button. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchViewCloseIconExistsOnlyToClearQueries() {
        mTestHelper.setPasswordSourceWithMultipleEntries(GREEK_GODS);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        // Trigger search which shouldn't have the button yet.
        onView(withSearchMenuIdOrText()).perform(click());
        ViewUtils.waitForViewCheckingState(
                withId(R.id.search_close_btn), VIEW_INVISIBLE | VIEW_GONE | VIEW_NULL);

        // Type something and see the button appear.
        onView(withId(R.id.search_src_text))
                // Trigger search which shouldn't have the button yet.
                .perform(click(), typeText("Zeu"), closeSoftKeyboard());
        onView(withId(R.id.search_close_btn)).check(matches(isDisplayed()));

        // Clear the search which should hide the button again.
        onView(withId(R.id.search_close_btn)).perform(click()); // Clear search.
        ViewUtils.waitForViewCheckingState(
                withId(R.id.search_close_btn), VIEW_INVISIBLE | VIEW_GONE | VIEW_NULL);
    }

    /**
     * Check that the changed color of the loaded Drawable does not persist for other uses of the
     * drawable. This is not implicitly true as a loaded Drawable is by default only a reference to
     * the globally defined resource.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testSearchIconColorAffectsOnlyLocalSearchDrawable() {
        // Open the password preferences and remember the applied color filter.
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);
        final PasswordSettings f = mSettingsActivityTestRule.getFragment();
        onView(withId(R.id.search_button)).check(matches(isDisplayed()));
        final AtomicReference<ColorFilter> passwordSearchFilter = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Drawable drawable =
                            f.getMenuForTesting().findItem(R.id.menu_id_search).getIcon();
                    passwordSearchFilter.set(DrawableCompat.getColorFilter(drawable));
                });

        // Now launch a non-empty History activity.
        StubbedHistoryProvider mHistoryProvider = new StubbedHistoryProvider();
        mHistoryProvider.addItem(StubbedHistoryProvider.createHistoryItem(0, new Date().getTime()));
        mHistoryProvider.addItem(StubbedHistoryProvider.createHistoryItem(1, new Date().getTime()));
        HistoryContentManager.setProviderForTests(mHistoryProvider);
        mHistoryActivityTestRule.launchActivity(null);

        // Find the search view to ensure that the set color filter is different from the saved one.
        final AtomicReference<ColorFilter> historySearchFilter = new AtomicReference<>();
        onView(withId(R.id.search_menu_id)).check(matches(isDisplayed()));
        onView(withId(R.id.search_menu_id))
                .check(
                        (searchMenuItem, e) -> {
                            Drawable drawable =
                                    ((ActionMenuItemView) searchMenuItem).getItemData().getIcon();
                            historySearchFilter.set(DrawableCompat.getColorFilter(drawable));
                            assertThat(
                                    historySearchFilter.get(),
                                    anyOf(
                                            is(nullValue()),
                                            is(not(sameInstance(passwordSearchFilter.get())))));
                        });

        // Close the activity and check that the icon in the password preferences has not changed.
        mHistoryActivityTestRule.getActivity().finish();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ColorFilter colorFilter =
                            DrawableCompat.getColorFilter(
                                    f.getMenuForTesting().findItem(R.id.menu_id_search).getIcon());
                    assertThat(
                            colorFilter,
                            anyOf(is(nullValue()), is(sameInstance(passwordSearchFilter.get()))));
                    assertThat(
                            colorFilter,
                            anyOf(
                                    is(nullValue()),
                                    is(not(sameInstance(historySearchFilter.get())))));
                });
    }

    /**
     * Check that the filtered password list persists after the user had inspected a single result.
     *
     * <p>TODO(crbug.com/40763233): Move this test to a full integration test which spins up native
     * and actually has stored passwords.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    @DisabledTest(message = "crbug/1202907 - The edit UI is now launched via native.")
    @RequiresRestart("crbug/1137002 - Figure out why this flakes as a batched test.")
    public void testSearchResultsPersistAfterEntryInspection() {
        mTestHelper.setPasswordSourceWithMultipleEntries(GREEK_GODS);
        mTestHelper.setPasswordExceptions(new String[] {"http://exclu.de", "http://not-inclu.de"});
        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);
        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        // Open the search and filter all but "Zeus".
        onView(withSearchMenuIdOrText()).perform(click());

        onViewWaiting(withId(R.id.search_src_text));
        onView(withId(R.id.search_src_text)).perform(click(), typeText("Zeu"), closeSoftKeyboard());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        onView(withText(R.string.passwords_auto_signin_title)).check(doesNotExist());
        onView(withText(ZEUS_ON_EARTH.getUserName())).check(matches(isDisplayed()));
        onView(withText(PHOBOS_AT_OLYMP.getUserName())).check(doesNotExist());
        onView(withText(HADES_AT_UNDERWORLD.getUrl())).check(doesNotExist());

        // Click "Zeus" to open edit field and verify the password. Pretend the user just passed the
        // reauthentication challenge.
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthenticationManager.ReauthScope.ONE_AT_A_TIME);
        Instrumentation.ActivityMonitor monitor =
                InstrumentationRegistry.getInstrumentation()
                        .addMonitor(new IntentFilter(Intent.ACTION_VIEW), null, false);
        onView(withText(ZEUS_ON_EARTH.getUserName())).perform(click());
        monitor.waitForActivityWithTimeout(UI_UPDATING_TIMEOUT_MS);
        Assert.assertEquals("Monitor for has not been called", 1, monitor.getHits());
        InstrumentationRegistry.getInstrumentation().removeMonitor(monitor);
        onView(withContentDescription(R.string.password_entry_viewer_show_stored_password))
                .perform(click());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        onView(withText(ZEUS_ON_EARTH.getPassword())).check(matches(isDisplayed()));
        onView(withContentDescription(R.string.abc_action_bar_up_description))
                .perform(click()); // Go back to the search list.
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        onView(withText(R.string.passwords_auto_signin_title)).check(doesNotExist());
        onView(withText(ZEUS_ON_EARTH.getUserName())).check(matches(isDisplayed()));
        onView(withText(PHOBOS_AT_OLYMP.getUserName())).check(doesNotExist());
        onView(withText(HADES_AT_UNDERWORLD.getUrl())).check(doesNotExist());
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        // The search bar should still be open and still display the search query.
        onViewWaiting(allOf(withId(R.id.search_src_text), withText("Zeu")));
        onView(withId(R.id.search_src_text)).check(matches(withText("Zeu")));
    }

    /**
     * Looks for the search icon by id or by its title.
     *
     * @return Returns either the icon button or the menu option.
     */
    private static Matcher<View> withSearchMenuIdOrText() {
        return withMenuIdOrText(R.id.menu_id_search, R.string.search);
    }

    /**
     * Looks for the icon by id. If it cannot be found, it's probably hidden in the overflow menu.
     * In that case, open the menu and search for its title.
     *
     * @return Returns either the icon button or the menu option.
     */
    private static Matcher<View> withMenuIdOrText(@IdRes int actionId, @StringRes int actionLabel) {
        Matcher<View> matcher = withId(actionId);
        try {
            Espresso.onView(matcher).check(matches(isDisplayed()));
            return matcher;
        } catch (Exception NoMatchingViewException) {
            openActionBarOverflowOrOptionsMenu(
                    InstrumentationRegistry.getInstrumentation().getTargetContext());
            return withText(actionLabel);
        }
    }
}