chromium/chrome/android/javatests/src/org/chromium/chrome/browser/app/appmenu/TabbedAppMenuTest.java

// Copyright 2014 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.app.appmenu;

import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import android.content.res.Configuration;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ListView;

import androidx.test.filters.LargeTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matchers;
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.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.bookmarks.PowerBookmarkUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.layouts.LayoutTestUtils;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridge;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridgeJni;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.quick_delete.QuickDeleteMetricsDelegate;
import org.chromium.chrome.browser.sync.FakeSyncServiceImpl;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler;
import org.chromium.chrome.browser.ui.appmenu.AppMenuItemProperties;
import org.chromium.chrome.browser.ui.appmenu.AppMenuTestSupport;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ActivityTestUtils;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.MenuUtils;
import org.chromium.chrome.test.util.browser.signin.SigninTestRule;
import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.DeviceRestriction;
import org.chromium.ui.test.util.GmsCoreVersionRestriction;
import org.chromium.ui.test.util.UiRestriction;

import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;

/** Tests tabbed mode app menu popup. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class TabbedAppMenuTest {
    private static final int RENDER_TEST_REVISION = 2;

    private static final String RENDER_TEST_DESCRIPTION =
            "Badge on settings menu item icon on identity and sync errors.";

    private static final String TEST_URL = UrlUtils.encodeHtmlDataUri("<html>foo</html>");

    @Rule
    public final ChromeTabbedActivityTestRule mActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule public final SigninTestRule mSigninTestRule = new SigninTestRule();

    @Rule
    public final ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setRevision(RENDER_TEST_REVISION)
                    .setDescription(RENDER_TEST_DESCRIPTION)
                    .setBugComponent(ChromeRenderTestRule.Component.UI_BROWSER_MOBILE_APP_MENU)
                    .build();

    @Rule public final JniMocker mJniMocker = new JniMocker();

    @Mock private PasswordManagerUtilBridge.Natives mPasswordManagerUtilBridgeJniMock;

    private AppMenuHandler mAppMenuHandler;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        // Prevent "GmsCore outdated" error from being exposed in bots with old version.
        mJniMocker.mock(PasswordManagerUtilBridgeJni.TEST_HOOKS, mPasswordManagerUtilBridgeJniMock);
        when(mPasswordManagerUtilBridgeJniMock.isGmsCoreUpdateRequired(any(), any()))
                .thenReturn(false);

        PowerBookmarkUtils.setPriceTrackingEligibleForTesting(true);

        // We need list selection; ensure we are not in touch mode.
        InstrumentationRegistry.getInstrumentation().setInTouchMode(false);

        CompositorAnimationHandler.setTestingMode(true);

        mActivityTestRule.startMainActivityWithURL(TEST_URL);

        AppMenuTestSupport.overrideOnOptionItemSelectedListener(
                mActivityTestRule.getAppMenuCoordinator(), unused -> {});
        mAppMenuHandler = mActivityTestRule.getAppMenuCoordinator().getAppMenuHandler();

        showAppMenuAndAssertMenuShown();

        PostTask.runOrPostTask(TaskTraits.UI_DEFAULT, () -> getListView().setSelection(0));
        CriteriaHelper.pollInstrumentationThread(
                () -> Criteria.checkThat(getCurrentFocusedRow(), Matchers.is(0)));
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    }

    @After
    public void tearDown() {
        ActivityTestUtils.clearActivityOrientation(mActivityTestRule.getActivity());

        CompositorAnimationHandler.setTestingMode(false);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    WebsitePreferenceBridge.setCategoryEnabled(
                            ProfileManager.getLastUsedRegularProfile(),
                            ContentSettingsType.REQUEST_DESKTOP_SITE,
                            false);
                });
    }

    /** Verify opening a new tab from the menu. */
    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    public void testMenuNewTab() {
        final int tabCountBefore = mActivityTestRule.getActivity().getCurrentTabModel().getCount();
        ChromeTabUtils.newTabFromMenu(
                InstrumentationRegistry.getInstrumentation(),
                (ChromeTabbedActivity) mActivityTestRule.getActivity());
        final int tabCountAfter = mActivityTestRule.getActivity().getCurrentTabModel().getCount();
        Assert.assertTrue(
                "Expected: " + (tabCountBefore + 1) + " Got: " + tabCountAfter,
                tabCountBefore + 1 == tabCountAfter);
    }

    /**
     * Test bounds when accessing the menu through the keyboard. Make sure that the menu stays open
     * when trying to move past the first and last items.
     */
    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    public void testKeyboardMenuBoundaries() {
        moveToBoundary(false, true);
        assertEquals(getCount() - 1, getCurrentFocusedRow());
        moveToBoundary(true, true);
        assertEquals(0, getCurrentFocusedRow());
        moveToBoundary(false, true);
        assertEquals(getCount() - 1, getCurrentFocusedRow());
    }

    /** Test that typing ENTER immediately opening the menu works. */
    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    public void testKeyboardMenuEnterOnOpen() {
        hitEnterAndAssertAppMenuDismissed();
    }

    /** Test that hitting ENTER past the top item doesn't crash Chrome. */
    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    public void testKeyboardEnterAfterMovePastTopItem() {
        moveToBoundary(true, true);
        assertEquals(0, getCurrentFocusedRow());
        hitEnterAndAssertAppMenuDismissed();
    }

    /**
     * Test that hitting ENTER past the bottom item doesn't crash Chrome. Catches regressions for
     * http://crbug.com/181067
     */
    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    public void testKeyboardEnterAfterMovePastBottomItem() {
        moveToBoundary(false, true);
        assertEquals(getCount() - 1, getCurrentFocusedRow());
        hitEnterAndAssertAppMenuDismissed();
    }

    /**
     * Test that hitting ENTER on the top item actually triggers the top item. Catches regressions
     * for https://crbug.com/191239 for shrunken menus.
     */
    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    public void testKeyboardMenuEnterOnTopItemLandscape() {
        ActivityTestUtils.rotateActivityToOrientation(
                mActivityTestRule.getActivity(), Configuration.ORIENTATION_LANDSCAPE);
        showAppMenuAndAssertMenuShown();
        moveToBoundary(true, false);
        assertEquals(0, getCurrentFocusedRow());
        hitEnterAndAssertAppMenuDismissed();
    }

    /** Test that hitting ENTER on the top item doesn't crash Chrome. */
    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    @Restriction(DeviceRestriction.RESTRICTION_TYPE_NON_AUTO)
    public void testKeyboardMenuEnterOnTopItemPortrait() {
        ActivityTestUtils.rotateActivityToOrientation(
                mActivityTestRule.getActivity(), Configuration.ORIENTATION_PORTRAIT);
        showAppMenuAndAssertMenuShown();
        moveToBoundary(true, false);
        assertEquals(0, getCurrentFocusedRow());
        hitEnterAndAssertAppMenuDismissed();
    }

    @Test
    @SmallTest
    @Feature({"Browser", "Main"})
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    public void testHideMenuOnToggleOverview() throws TimeoutException {
        // App menu is shown during setup.
        Assert.assertTrue("App menu should be showing.", mAppMenuHandler.isAppMenuShowing());
        Assert.assertFalse(
                "Overview shouldn't be showing.",
                mActivityTestRule
                        .getActivity()
                        .getLayoutManager()
                        .isLayoutVisible(LayoutType.TAB_SWITCHER));

        LayoutTestUtils.startShowingAndWaitForLayout(
                mActivityTestRule.getActivity().getLayoutManager(), LayoutType.TAB_SWITCHER, false);

        Assert.assertTrue(
                "Overview should be showing.",
                mActivityTestRule
                        .getActivity()
                        .getLayoutManager()
                        .isLayoutVisible(LayoutType.TAB_SWITCHER));
        Assert.assertFalse("App menu shouldn't be showing.", mAppMenuHandler.isAppMenuShowing());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(
                            "App menu should be allowed to show.",
                            AppMenuTestSupport.shouldShowAppMenu(
                                    mActivityTestRule.getAppMenuCoordinator()));
                });
        showAppMenuAndAssertMenuShown();

        LayoutTestUtils.startShowingAndWaitForLayout(
                mActivityTestRule.getActivity().getLayoutManager(), LayoutType.BROWSING, false);
        Assert.assertFalse(
                "Overview shouldn't be showing.",
                mActivityTestRule
                        .getActivity()
                        .getLayoutManager()
                        .isLayoutVisible(LayoutType.TAB_SWITCHER));
        CriteriaHelper.pollUiThread(
                () -> !mAppMenuHandler.isAppMenuShowing(), "App menu shouldn't be showing.");
    }

    @Test
    @SmallTest
    @Feature({"Browser", "Main", "Bookmark", "RenderTest"})
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    public void testBookmarkMenuItem() throws IOException {
        PropertyModel bookmarkStarPropertyModel =
                AppMenuTestSupport.getMenuItemPropertyModel(
                        mActivityTestRule.getAppMenuCoordinator(), R.id.bookmark_this_page_id);
        Assert.assertFalse(
                "Bookmark item should not be checked.",
                bookmarkStarPropertyModel.get(AppMenuItemProperties.CHECKED));
        assertEquals(
                "Incorrect content description.",
                mActivityTestRule.getActivity().getString(R.string.menu_bookmark),
                bookmarkStarPropertyModel.get(AppMenuItemProperties.TITLE_CONDENSED));
        mRenderTestRule.render(getListView().getChildAt(0), "rounded_corner_icon_row");

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
        AppMenuPropertiesDelegateImpl.setPageBookmarkedForTesting(true);
        showAppMenuAndAssertMenuShown();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        bookmarkStarPropertyModel =
                AppMenuTestSupport.getMenuItemPropertyModel(
                        mActivityTestRule.getAppMenuCoordinator(), R.id.bookmark_this_page_id);
        Assert.assertTrue(
                "Bookmark item should be checked.",
                bookmarkStarPropertyModel.get(AppMenuItemProperties.CHECKED));
        assertEquals(
                "Incorrect content description for bookmarked page.",
                mActivityTestRule.getActivity().getString(R.string.edit_bookmark),
                bookmarkStarPropertyModel.get(AppMenuItemProperties.TITLE_CONDENSED));
        mRenderTestRule.render(
                getListView().getChildAt(0), "rounded_corner_icon_row_page_bookmarked");
    }

    @Test
    @SmallTest
    @Feature({"Browser", "Main", "RenderTest"})
    public void testDividerLineMenuItem() throws IOException {
        int firstDividerLineIndex =
                AppMenuTestSupport.findIndexOfMenuItemById(
                        mActivityTestRule.getAppMenuCoordinator(), R.id.divider_line_id);
        Assert.assertTrue("No divider line found.", firstDividerLineIndex != -1);
        mRenderTestRule.render(getListView().getChildAt(firstDividerLineIndex), "divider_line");
    }

    @Test
    @SmallTest
    @Feature({"Browser", "Main", "RenderTest"})
    @Restriction(UiRestriction.RESTRICTION_TYPE_PHONE)
    public void testRequestDesktopSiteMenuItem_checkbox() throws IOException {
        Tab tab = mActivityTestRule.getActivity().getTabModelSelector().getCurrentTab();
        boolean isRequestDesktopSite =
                tab.getWebContents().getNavigationController().getUseDesktopUserAgent();
        Assert.assertFalse("Default to request mobile site.", isRequestDesktopSite);

        int requestDesktopSiteIndex =
                AppMenuTestSupport.findIndexOfMenuItemById(
                        mActivityTestRule.getAppMenuCoordinator(),
                        R.id.request_desktop_site_row_menu_id);
        Assert.assertNotEquals("No request desktop site row found.", -1, requestDesktopSiteIndex);

        Callable<Boolean> isVisible =
                () -> {
                    int visibleStart = getListView().getFirstVisiblePosition();
                    int visibleEnd = visibleStart + getListView().getChildCount() - 1;
                    return requestDesktopSiteIndex >= visibleStart
                            && requestDesktopSiteIndex <= visibleEnd;
                };
        CriteriaHelper.pollUiThread(() -> getListView().getChildAt(0) != null);
        if (!ThreadUtils.runOnUiThreadBlocking(isVisible)) {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> getListView().smoothScrollToPosition(requestDesktopSiteIndex));
            CriteriaHelper.pollUiThread(isVisible);
        }
        mRenderTestRule.render(
                getListView()
                        .getChildAt(
                                requestDesktopSiteIndex - getListView().getFirstVisiblePosition()),
                "request_desktop_site_uncheck");

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    WebsitePreferenceBridge.setCategoryEnabled(
                            ProfileManager.getLastUsedRegularProfile(),
                            ContentSettingsType.REQUEST_DESKTOP_SITE,
                            true);
                    tab.reload();
                });
        ChromeTabUtils.waitForTabPageLoaded(tab, TEST_URL);
        isRequestDesktopSite =
                tab.getWebContents().getNavigationController().getUseDesktopUserAgent();
        Assert.assertTrue("Should request desktop site.", isRequestDesktopSite);

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
        showAppMenuAndAssertMenuShown();
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();

        CriteriaHelper.pollUiThread(() -> getListView().getChildAt(0) != null);
        if (!ThreadUtils.runOnUiThreadBlocking(isVisible)) {
            ThreadUtils.runOnUiThreadBlocking(
                    () -> getListView().smoothScrollToPosition(requestDesktopSiteIndex));
            CriteriaHelper.pollUiThread(isVisible);
        }
        mRenderTestRule.render(
                getListView()
                        .getChildAt(
                                requestDesktopSiteIndex - getListView().getFirstVisiblePosition()),
                "request_mobile_site_check");
    }

    @Test
    @LargeTest
    @Feature({"Browser", "Main", "QuickDelete", "RenderTest"})
    @EnableFeatures(ChromeFeatureList.QUICK_DELETE_FOR_ANDROID)
    public void testQuickDeleteMenu_Shown() throws IOException {
        showAppMenuAndAssertMenuShown();
        int quickDeletePosition =
                AppMenuTestSupport.findIndexOfMenuItemById(
                        mActivityTestRule.getAppMenuCoordinator(), R.id.quick_delete_menu_id);
        mRenderTestRule.render(getListView().getChildAt(quickDeletePosition), "quick_delete");
    }

    @Test
    @SmallTest
    @Feature({"Browser", "Main", "QuickDelete"})
    @EnableFeatures(ChromeFeatureList.QUICK_DELETE_FOR_ANDROID)
    public void testQuickDeleteMenu_entryFromMenuItemHistogram() throws IOException {
        HistogramWatcher histogramWatcher =
                HistogramWatcher.newSingleRecordWatcher(
                        QuickDeleteMetricsDelegate.HISTOGRAM_NAME,
                        QuickDeleteMetricsDelegate.QuickDeleteAction.MENU_ITEM_CLICKED);

        MenuUtils.invokeCustomMenuActionSync(
                InstrumentationRegistry.getInstrumentation(),
                mActivityTestRule.getActivity(),
                R.id.quick_delete_menu_id);

        histogramWatcher.assertExpected();
    }

    @Test
    @LargeTest
    @Feature({"Browser", "Main", "QuickDelete"})
    @EnableFeatures(ChromeFeatureList.QUICK_DELETE_FOR_ANDROID)
    public void testQuickDeleteMenu_NotShownInIncognito() {
        // Hide first any shown app menu as it can interfere with this test.
        hitEnterAndAssertAppMenuDismissed();

        mActivityTestRule.newIncognitoTabFromMenu();
        showAppMenuAndAssertMenuShown();
        assertEquals(
                -1,
                AppMenuTestSupport.findIndexOfMenuItemById(
                        mActivityTestRule.getAppMenuCoordinator(), R.id.quick_delete_menu_id));
    }

    @Test
    @LargeTest
    @Feature({"Browser", "Main", "QuickDelete"})
    @DisableFeatures(ChromeFeatureList.QUICK_DELETE_FOR_ANDROID)
    public void testQuickDeleteMenu_NotShown() throws IOException {
        showAppMenuAndAssertMenuShown();
        assertEquals(
                -1,
                AppMenuTestSupport.findIndexOfMenuItemById(
                        mActivityTestRule.getAppMenuCoordinator(), R.id.quick_delete_menu_id));
    }

    @Test
    @LargeTest
    @Feature({"Browser", "Main", "RenderTest"})
    public void testSettingsMenuItem_NoBadgeShownForNotSignedInUsers() throws IOException {
        View view = getSettingsMenuItemView();
        Assert.assertNull(view.findViewById(R.id.menu_item_text).getContentDescription());
        mRenderTestRule.render(view, "settings_menu_item_not_signed_in_user");
    }

    @Test
    @LargeTest
    @Feature({"Browser", "Main", "RenderTest"})
    public void testSettingsMenuItem_BadgeShownForSignedInUsersOnIdentityError()
            throws IOException {
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());

        FakeSyncServiceImpl fakeSyncService =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            FakeSyncServiceImpl fakeSyncServiceImpl = new FakeSyncServiceImpl();
                            SyncServiceFactory.setInstanceForTesting(fakeSyncServiceImpl);
                            return fakeSyncServiceImpl;
                        });
        // Fake an identity error.
        fakeSyncService.setRequiresClientUpgrade(true);
        // Sign in and wait for sync machinery to be active.
        mSigninTestRule.addTestAccountThenSignin();

        showAppMenuAndAssertMenuShown();
        View view = getSettingsMenuItemView();
        assertEquals(
                "Content description should mention an error.",
                view.findViewById(R.id.menu_item_text).getContentDescription(),
                mActivityTestRule.getActivity().getString(R.string.menu_settings_account_error));
        mRenderTestRule.render(view, "settings_menu_item_signed_in_user_identity_error");
    }

    @Test
    @LargeTest
    @Feature({"Browser", "Main", "RenderTest"})
    public void testSettingsMenuItem_NoBadgeShownForSignedInUsersIfNoError() throws IOException {
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
        // Sign in and wait for sync machinery to be active.
        mSigninTestRule.addTestAccountThenSignin();

        showAppMenuAndAssertMenuShown();
        View view = getSettingsMenuItemView();
        Assert.assertNull(view.findViewById(R.id.menu_item_text).getContentDescription());
        mRenderTestRule.render(view, "settings_menu_item_signed_in_user_no_error");
    }

    @Test
    @LargeTest
    @Feature({"Browser", "Main", "RenderTest"})
    public void testSettingsMenuItem_BadgeShownForSyncingUsersOnSyncError() throws IOException {
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
        FakeSyncServiceImpl fakeSyncService =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            FakeSyncServiceImpl fakeSyncServiceImpl = new FakeSyncServiceImpl();
                            SyncServiceFactory.setInstanceForTesting(fakeSyncServiceImpl);
                            return fakeSyncServiceImpl;
                        });
        // Fake an identity error.
        fakeSyncService.setRequiresClientUpgrade(true);
        // Sign in and wait for sync machinery to be active.
        mSigninTestRule.addTestAccountThenSigninAndEnableSync();

        showAppMenuAndAssertMenuShown();
        View view = getSettingsMenuItemView();
        assertEquals(
                "Content description should mention an error.",
                view.findViewById(R.id.menu_item_text).getContentDescription(),
                mActivityTestRule.getActivity().getString(R.string.menu_settings_account_error));
        mRenderTestRule.render(view, "settings_menu_item_syncing_user_sync_error");
    }

    @Test
    @LargeTest
    @Restriction(GmsCoreVersionRestriction.RESTRICTION_TYPE_VERSION_GE_22W30)
    @Feature({"Browser", "Main", "RenderTest"})
    public void testSettingsMenuItem_NoBadgeShownForSyncingUsersIfNoError() throws IOException {
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
        // Sign in and wait for sync machinery to be active.
        mSigninTestRule.addTestAccountThenSigninAndEnableSync();

        showAppMenuAndAssertMenuShown();
        View view = getSettingsMenuItemView();
        Assert.assertNull(view.findViewById(R.id.menu_item_text).getContentDescription());
        mRenderTestRule.render(view, "settings_menu_item_syncing_user_no_error");
    }

    private void showAppMenuAndAssertMenuShown() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    AppMenuTestSupport.showAppMenu(
                            mActivityTestRule.getAppMenuCoordinator(), null, false);
                    Assert.assertTrue(mAppMenuHandler.isAppMenuShowing());
                });
    }

    private void hitEnterAndAssertAppMenuDismissed() {
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
        pressKey(KeyEvent.KEYCODE_ENTER);
        CriteriaHelper.pollInstrumentationThread(
                () -> !mAppMenuHandler.isAppMenuShowing(), "AppMenu did not dismiss");
    }

    private void moveToBoundary(boolean towardsTop, boolean movePast) {
        // Move to the boundary.
        final int end = towardsTop ? 0 : getCount() - 1;
        int increment = towardsTop ? -1 : 1;
        for (int index = getCurrentFocusedRow(); index != end; index += increment) {
            pressKey(towardsTop ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN);
            final int expectedPosition = index + increment;
            CriteriaHelper.pollInstrumentationThread(
                    () -> {
                        Criteria.checkThat(getCurrentFocusedRow(), Matchers.is(expectedPosition));
                    });
        }

        // Try moving past it by one.
        if (movePast) {
            pressKey(towardsTop ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN);
            CriteriaHelper.pollInstrumentationThread(
                    () -> Criteria.checkThat(getCurrentFocusedRow(), Matchers.is(end)));
        }

        // The menu should stay open.
        Assert.assertTrue(mAppMenuHandler.isAppMenuShowing());
    }

    private void pressKey(final int keycode) {
        final View view = getListView();
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keycode));
                    view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keycode));
                });
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    }

    private int getCurrentFocusedRow() {
        ListView listView = getListView();
        if (listView == null) return ListView.INVALID_POSITION;
        return listView.getSelectedItemPosition();
    }

    private int getCount() {
        ListView listView = getListView();
        if (listView == null) return 0;
        return listView.getCount();
    }

    private ListView getListView() {
        return AppMenuTestSupport.getListView(mActivityTestRule.getAppMenuCoordinator());
    }

    private View getSettingsMenuItemView() {
        int position =
                AppMenuTestSupport.findIndexOfMenuItemById(
                        mActivityTestRule.getAppMenuCoordinator(), R.id.preferences_id);
        Assert.assertTrue("No settings menu item found.", position != -1);

        CriteriaHelper.pollUiThread(() -> getListView().getChildAt(0) != null);

        Callable<Boolean> isVisible =
                () -> {
                    int visibleStart = getListView().getFirstVisiblePosition();
                    int visibleEnd = visibleStart + getListView().getChildCount() - 1;
                    return position >= visibleStart && position <= visibleEnd;
                };

        if (!ThreadUtils.runOnUiThreadBlocking(isVisible)) {
            ThreadUtils.runOnUiThreadBlocking(() -> getListView().setSelection(position));
            CriteriaHelper.pollUiThread(isVisible);
        }

        View view =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return getListView()
                                    .getChildAt(position - getListView().getFirstVisiblePosition());
                        });
        Assert.assertNotNull("No settings menu item view found.", view);
        return view;
    }
}