chromium/chrome/browser/ui/android/appmenu/internal/java/src/org/chromium/chrome/browser/ui/appmenu/AppMenuTest.java

// Copyright 2019 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.ui.appmenu;

import static org.mockito.ArgumentMatchers.eq;

import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build.VERSION_CODES;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisableIf;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.LifecycleObserver;
import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler.AppMenuItemType;
import org.chromium.chrome.browser.ui.appmenu.test.R;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.browser_ui.widget.chips.ChipView;
import org.chromium.components.browser_ui.widget.highlight.ViewHighlighterTestUtils;
import org.chromium.ui.modelutil.MVCListAdapter;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.test.util.BlankUiTestActivity;
import org.chromium.ui.test.util.BlankUiTestActivityTestCase;
import org.chromium.ui.test.util.UiDisableIf;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

/**
 * Integration tests the app menu popup. Covers AppMenuCoordinatorImpl and public interface for
 * AppMenuHandlerImpl.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@Batch(Batch.PER_CLASS)
public class AppMenuTest extends BlankUiTestActivityTestCase {
    private AppMenuCoordinatorImpl mAppMenuCoordinator;
    private AppMenuHandlerImpl mAppMenuHandler;
    private TestAppMenuPropertiesDelegate mPropertiesDelegate;
    private TestAppMenuDelegate mDelegate;
    private TestAppMenuObserver mMenuObserver;
    private TestActivityLifecycleDispatcher mLifecycleDispatcher;
    private TestMenuButtonDelegate mTestMenuButtonDelegate;

    @Mock private Canvas mCanvas;
    // Tell R8 not to break the ability to mock the class.
    @Mock private AppMenu mUnused;

    @BeforeClass
    public static void setUpBeforeActivityLaunched() {
        BlankUiTestActivity.setTestLayout(R.layout.test_app_menu_activity_layout);
    }

    @Override
    public void setUpTest() throws Exception {
        super.setUpTest();
        mCanvas = Mockito.mock(Canvas.class);
        ThreadUtils.runOnUiThreadBlocking(this::setUpTestOnUiThread);
        mLifecycleDispatcher.observerRegisteredCallbackHelper.waitForCallback(0);
    }

    @AfterClass
    public static void tearDownAfterActivityDestroyed() {}

    private void setUpTestOnUiThread() {
        mLifecycleDispatcher = new TestActivityLifecycleDispatcher();
        mDelegate = new TestAppMenuDelegate();
        mTestMenuButtonDelegate = new TestMenuButtonDelegate();
        mAppMenuCoordinator =
                new AppMenuCoordinatorImpl(
                        getActivity(),
                        mLifecycleDispatcher,
                        mTestMenuButtonDelegate,
                        mDelegate,
                        getActivity().getWindow().getDecorView(),
                        getActivity().findViewById(R.id.menu_anchor_stub),
                        this::getAppRect);
        mAppMenuHandler = mAppMenuCoordinator.getAppMenuHandlerImplForTesting();
        mMenuObserver = new TestAppMenuObserver();
        mAppMenuCoordinator.getAppMenuHandler().addObserver(mMenuObserver);
        mPropertiesDelegate =
                (TestAppMenuPropertiesDelegate) mAppMenuCoordinator.getAppMenuPropertiesDelegate();
    }

    private Rect getAppRect() {
        Rect appRect = new Rect();
        getActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(appRect);
        return appRect;
    }

    @Test
    @MediumTest
    public void testShowHideAppMenu() throws TimeoutException {
        showMenuAndAssert();

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
        mMenuObserver.menuHiddenCallback.waitForCallback(0);

        Assert.assertEquals(
                "Incorrect number of calls to #onMenuDismissed after hide",
                1,
                mPropertiesDelegate.menuDismissedCallback.getCallCount());

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuCoordinator.destroy());
        Assert.assertEquals(
                "Incorrect number of calls to #onMenuDismissed after destroy",
                1,
                mPropertiesDelegate.menuDismissedCallback.getCallCount());
    }

    @Test
    @MediumTest
    public void testHideAppMenuMultiple() throws TimeoutException {
        showMenuAndAssert();

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.getAppMenu().dismiss());
        mMenuObserver.menuHiddenCallback.waitForCallback(0);

        Assert.assertEquals(
                "Incorrect number of calls to #onMenuDismissed after first call",
                1,
                mPropertiesDelegate.menuDismissedCallback.getCallCount());

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.getAppMenu().dismiss());
        Assert.assertEquals(
                "Incorrect number of calls to #onMenuDismissed after second call",
                1,
                mPropertiesDelegate.menuDismissedCallback.getCallCount());
    }

    @Test
    @MediumTest
    public void testShowAppMenu_AnchorTop() throws TimeoutException {
        AppMenuCoordinatorImpl.setHasPermanentMenuKeyForTesting(false);
        showMenuAndAssert();

        View topAnchor = getActivity().findViewById(R.id.top_button);
        Rect viewRect = getViewLocationRect(topAnchor);
        Rect popupRect = getPopupLocationRect();

        // Check that top right corner of app menu aligns with the top right corner of the anchor.
        int alignmentSlop = viewRect.bottom - viewRect.top;
        Assert.assertEquals(
                "Popup should overlap top anchor. Anchor rect: "
                        + viewRect
                        + ", popup rect: "
                        + popupRect,
                viewRect.top,
                popupRect.top,
                alignmentSlop);
        Assert.assertTrue(
                "Popup should overlap top anchor. Anchor rect: "
                        + viewRect
                        + ", popup rect: "
                        + popupRect,
                viewRect.top <= popupRect.top);
        Assert.assertEquals(
                "Popup should be aligned with right of anchor. Anchor rect: "
                        + viewRect
                        + ", popup rect: "
                        + popupRect,
                viewRect.right,
                popupRect.right);
    }

    @Test
    @MediumTest
    public void testShowAppMenu_PermanentButton() throws TimeoutException {
        AppMenuCoordinatorImpl.setHasPermanentMenuKeyForTesting(true);
        showMenuAndAssert();

        View anchorStub = getActivity().findViewById(R.id.menu_anchor_stub);
        Rect viewRect = getViewLocationRect(anchorStub);
        Rect popupRect = getPopupLocationRect();

        // Check a basic alignment property. Full coverage checked in unit tests.
        Assert.assertNotEquals(
                "Popup should be offset from right of anchor."
                        + "Anchor rect: "
                        + viewRect
                        + ", popup rect: "
                        + popupRect,
                viewRect.right,
                popupRect.right);
    }

    @Test
    @MediumTest
    public void testShowDestroyAppMenu() throws TimeoutException {
        showMenuAndAssert();

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuCoordinator.destroy());

        Assert.assertEquals(
                "Incorrect number of calls to #onMenuDismissed after destroy",
                1,
                mPropertiesDelegate.menuDismissedCallback.getCallCount());
    }

    @Test
    @MediumTest
    public void testClickMenuItem() throws TimeoutException {
        showMenuAndAssert();

        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        AppMenuTestSupport.callOnItemClick(
                                mAppMenuCoordinator, R.id.menu_item_three));

        mDelegate.itemSelectedCallbackHelper.waitForCallback(0);
        Assert.assertEquals(
                "Incorrect id for last selected item.",
                R.id.menu_item_three,
                mDelegate.lastSelectedItemId);
    }

    @Test
    @MediumTest
    public void testClickMenuItem_Disabled() throws TimeoutException {
        showMenuAndAssert();

        ThreadUtils.runOnUiThreadBlocking(
                () -> AppMenuTestSupport.callOnItemClick(mAppMenuCoordinator, R.id.menu_item_two));

        Assert.assertEquals(
                "Item selected callback should not have been called.",
                0,
                mDelegate.itemSelectedCallbackHelper.getCallCount());
    }

    @Test
    @MediumTest
    public void testClickMenuItem_UsingPosition() throws TimeoutException {
        showMenuAndAssert();

        ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.getAppMenu().onItemClick(null, null, 0, 0));

        mDelegate.itemSelectedCallbackHelper.waitForCallback(0);
        Assert.assertEquals(
                "Incorrect id for last selected item.",
                R.id.menu_item_one,
                mDelegate.lastSelectedItemId);
    }

    @Test
    @MediumTest
    public void testLongClickMenuItem_Title() throws TimeoutException {
        mPropertiesDelegate.enableAppIconRow = true;
        showMenuAndAssert();
        AppMenu spiedMenu = Mockito.spy(mAppMenuHandler.getAppMenu());

        View dummyView = new View(getActivity());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    spiedMenu.onItemLongClick(
                            mAppMenuHandler.getAppMenu().getMenuItemPropertyModel(R.id.icon_one),
                            dummyView);
                });

        Mockito.verify(spiedMenu, Mockito.times(1)).showToastForItem("Icon One", dummyView);
    }

    @Test
    @MediumTest
    public void testLongClickMenuItem_TitleCondensed() throws TimeoutException {
        mPropertiesDelegate.enableAppIconRow = true;
        showMenuAndAssert();
        AppMenu spiedMenu = Mockito.spy(mAppMenuHandler.getAppMenu());

        View dummyView = new View(getActivity());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    spiedMenu.onItemLongClick(
                            mAppMenuHandler.getAppMenu().getMenuItemPropertyModel(R.id.icon_two),
                            dummyView);
                });

        Mockito.verify(spiedMenu, Mockito.times(1)).showToastForItem("2", dummyView);
    }

    @Test
    @MediumTest
    public void testLongClickMenuItem_Disabled() throws TimeoutException {
        mPropertiesDelegate.enableAppIconRow = true;
        showMenuAndAssert();
        AppMenu spiedMenu = Mockito.spy(mAppMenuHandler.getAppMenu());

        View dummyView = new View(getActivity());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    spiedMenu.onItemLongClick(
                            mAppMenuHandler.getAppMenu().getMenuItemPropertyModel(R.id.icon_three),
                            dummyView);
                });

        Mockito.verify(spiedMenu, Mockito.times(0))
                .showToastForItem(Mockito.any(CharSequence.class), Mockito.any(View.class));
    }

    @Test
    @MediumTest
    public void testAppMenuBlockers() throws TimeoutException {
        Assert.assertTrue(
                "App menu should be allowed to show, no blockers registered",
                AppMenuTestSupport.shouldShowAppMenu(mAppMenuCoordinator));

        AppMenuBlocker blocker1 = () -> false;
        AppMenuBlocker blocker2 = () -> true;

        mAppMenuCoordinator.registerAppMenuBlocker(blocker1);
        mAppMenuCoordinator.registerAppMenuBlocker(blocker2);
        Assert.assertFalse(
                "App menu should not be allowed to show, both blockers registered",
                AppMenuTestSupport.shouldShowAppMenu(mAppMenuCoordinator));
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuCoordinator.showAppMenuForKeyboardEvent());
        Assert.assertFalse(
                "App menu should not have been shown.", mAppMenuHandler.isAppMenuShowing());

        mAppMenuCoordinator.unregisterAppMenuBlocker(blocker1);
        Assert.assertTrue(
                "App menu should be allowed to show, only blocker2 registered",
                AppMenuTestSupport.shouldShowAppMenu(mAppMenuCoordinator));
        showMenuAndAssert();
    }

    @Test
    @MediumTest
    public void testSetMenuHighlight_StandardItem() throws TimeoutException {
        Assert.assertFalse(mMenuObserver.menuHighlighting);

        ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.setMenuHighlight(R.id.menu_item_one));
        mMenuObserver.menuHighlightChangedCallback.waitForCallback(0);
        Assert.assertTrue(mMenuObserver.menuHighlighting);

        showMenuAndAssert();

        View itemView = getViewAtPosition(0);
        checkHighlightOn(itemView);

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.clearMenuHighlight());
        mMenuObserver.menuHighlightChangedCallback.waitForCallback(1);
        Assert.assertFalse(mMenuObserver.menuHighlighting);
    }

    @Test
    @MediumTest
    public void testSetMenuHighlight_ChipItem() throws TimeoutException {
        mPropertiesDelegate.footerResourceId = R.layout.test_menu_footer;
        Assert.assertFalse(mMenuObserver.menuHighlighting);

        ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.setMenuHighlight(R.id.menu_footer_chip_view));
        mMenuObserver.menuHighlightChangedCallback.waitForCallback(0);
        Assert.assertTrue(mMenuObserver.menuHighlighting);

        showMenuAndAssert();
        mPropertiesDelegate.footerInflatedCallback.waitForCallback(0);

        ChipView chipView =
                (ChipView)
                        mAppMenuHandler
                                .getAppMenu()
                                .getListView()
                                .getRootView()
                                .findViewById(R.id.menu_footer_chip_view);
        checkHighlightOn(chipView);

        ViewHighlighterTestUtils.drawPulseDrawable(chipView, mCanvas);
        Mockito.verify(mCanvas)
                .drawRoundRect(
                        Mockito.any(),
                        eq((float) chipView.getCornerRadius()),
                        eq((float) chipView.getCornerRadius()),
                        Mockito.any());

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.clearMenuHighlight());
        mMenuObserver.menuHighlightChangedCallback.waitForCallback(1);
        Assert.assertFalse(mMenuObserver.menuHighlighting);
    }

    @Test
    @MediumTest
    public void testSetMenuHighlight_Icon() throws TimeoutException {
        mPropertiesDelegate.enableAppIconRow = true;

        Assert.assertFalse(mMenuObserver.menuHighlighting);

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.setMenuHighlight(R.id.icon_one));
        mMenuObserver.menuHighlightChangedCallback.waitForCallback(0);
        Assert.assertTrue(mMenuObserver.menuHighlighting);

        showMenuAndAssert();

        View itemView = ((LinearLayout) getViewAtPosition(3)).getChildAt(0);
        checkHighlightOn(itemView);

        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.clearMenuHighlight());
        mMenuObserver.menuHighlightChangedCallback.waitForCallback(1);
        Assert.assertFalse(mMenuObserver.menuHighlighting);
    }

    @Test
    @MediumTest
    public void testMenuItemContentChanged() throws TimeoutException {
        showMenuAndAssert();
        View itemView = getViewAtPosition(1);
        Assert.assertEquals(
                "Menu item text incorrect",
                "Menu Item Two",
                ((TextView) itemView.findViewById(R.id.menu_item_text)).getText());

        String newText = "Test!";
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mAppMenuHandler
                            .getAppMenu()
                            .getMenuItemPropertyModel(R.id.menu_item_two)
                            .set(AppMenuItemProperties.TITLE, newText);
                    mAppMenuHandler.menuItemContentChanged(R.id.menu_item_two);
                });

        itemView = getViewAtPosition(1);
        Assert.assertEquals(
                "Menu item text incorrect",
                newText,
                ((TextView) itemView.findViewById(R.id.menu_item_text)).getText());
    }

    @Test
    @MediumTest
    public void testMenuItemRemoved() throws TimeoutException, ExecutionException {
        showMenuAndAssert();
        Assert.assertEquals(3, mAppMenuHandler.getModelListForTesting().size());
        View itemView = getViewAtPosition(1);
        Assert.assertEquals(
                "Menu item text incorrect",
                "Menu Item Two",
                ((TextView) itemView.findViewById(R.id.menu_item_text)).getText());

        ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.getModelListForTesting().removeAt(1));

        itemView = getViewAtPosition(1);
        Assert.assertEquals(
                "Menu item text incorrect",
                "Menu Item Three",
                ((TextView) itemView.findViewById(R.id.menu_item_text)).getText());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            0,
                            mAppMenuHandler
                                    .getAppMenu()
                                    .getMenuItemPropertyModel(R.id.menu_item_one)
                                    .get(AppMenuItemProperties.POSITION));

                    Assert.assertEquals(
                            1,
                            mAppMenuHandler
                                    .getAppMenu()
                                    .getMenuItemPropertyModel(R.id.menu_item_three)
                                    .get(AppMenuItemProperties.POSITION));
                });
    }

    @Test
    @MediumTest
    public void testMenuItemRangeRemoved() throws TimeoutException, ExecutionException {
        showMenuAndAssert();
        Assert.assertEquals(3, mAppMenuHandler.getModelListForTesting().size());
        View itemView = getViewAtPosition(1);
        Assert.assertEquals(
                "Menu item text incorrect",
                "Menu Item Two",
                ((TextView) itemView.findViewById(R.id.menu_item_text)).getText());

        ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.getModelListForTesting().removeRange(0, 2));

        Assert.assertEquals(1, mAppMenuHandler.getModelListForTesting().size());
        itemView = getViewAtPosition(0);
        Assert.assertEquals(
                "Menu item text incorrect",
                "Menu Item Three",
                ((TextView) itemView.findViewById(R.id.menu_item_text)).getText());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            0,
                            mAppMenuHandler
                                    .getAppMenu()
                                    .getMenuItemPropertyModel(R.id.menu_item_three)
                                    .get(AppMenuItemProperties.POSITION));
                });
    }

    @Test
    @MediumTest
    public void testMenuItemAdded() throws TimeoutException {
        showMenuAndAssert();
        Assert.assertEquals(3, mAppMenuHandler.getModelListForTesting().size());
        View itemView = getViewAtPosition(1);
        Assert.assertEquals(
                "Menu item text incorrect",
                "Menu Item Two",
                ((TextView) itemView.findViewById(R.id.menu_item_text)).getText());

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyModel model =
                            new PropertyModel.Builder(AppMenuItemProperties.ALL_KEYS)
                                    .with(AppMenuItemProperties.MENU_ITEM_ID, 13)
                                    .with(AppMenuItemProperties.TITLE, "new item title")
                                    .build();
                    mAppMenuHandler
                            .getModelListForTesting()
                            .add(0, new MVCListAdapter.ListItem(AppMenuItemType.STANDARD, model));
                });
        // ensure clicking on the newly added item doesn't break anything
        ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.getAppMenu().onItemClick(null, null, 0, 0));

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PropertyModel m = mAppMenuHandler.getAppMenu().getMenuItemPropertyModel(13);
                    Assert.assertNotNull(m.get(AppMenuItemProperties.CLICK_HANDLER));
                    Assert.assertEquals(0, m.get(AppMenuItemProperties.POSITION));
                    Assert.assertEquals(
                            1,
                            mAppMenuHandler
                                    .getAppMenu()
                                    .getMenuItemPropertyModel(R.id.menu_item_one)
                                    .get(AppMenuItemProperties.POSITION));
                    Assert.assertEquals(
                            2,
                            mAppMenuHandler
                                    .getAppMenu()
                                    .getMenuItemPropertyModel(R.id.menu_item_two)
                                    .get(AppMenuItemProperties.POSITION));
                    Assert.assertEquals(
                            3,
                            mAppMenuHandler
                                    .getAppMenu()
                                    .getMenuItemPropertyModel(R.id.menu_item_three)
                                    .get(AppMenuItemProperties.POSITION));
                });
    }

    @Test
    @MediumTest
    public void testHeaderFooter() throws TimeoutException {
        mPropertiesDelegate.headerResourceId = R.layout.test_menu_header;
        mPropertiesDelegate.footerResourceId = R.layout.test_menu_footer;
        showMenuAndAssert();

        mPropertiesDelegate.headerInflatedCallback.waitForCallback(0);
        mPropertiesDelegate.footerInflatedCallback.waitForCallback(0);

        Assert.assertEquals(
                "Incorrect number of header views",
                1,
                mAppMenuHandler.getAppMenu().getListView().getHeaderViewsCount());
        Assert.assertNotNull(
                "Footer stub not inflated.",
                mAppMenuHandler
                        .getAppMenu()
                        .getPopup()
                        .getContentView()
                        .findViewById(R.id.app_menu_footer));
    }

    @Test
    @MediumTest
    public void testAppMenuHiddenOnStopWithNative() throws TimeoutException {
        showMenuAndAssert();
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.onStopWithNative());
        Assert.assertFalse(mAppMenuHandler.isAppMenuShowing());
    }

    @Test
    @MediumTest
    public void testAppMenuHiddenOnConfigurationChange() throws TimeoutException {
        showMenuAndAssert();
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.onConfigurationChanged(null));
        Assert.assertFalse(mAppMenuHandler.isAppMenuShowing());
    }

    @Test
    @MediumTest
    public void testAppMenuKeyEvent_HiddenOnHardwareButtonPress() throws Exception {
        showMenuAndAssert();

        AppMenu appMenu = mAppMenuHandler.getAppMenu();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    KeyEvent down = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU);
                    KeyEvent up = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU);
                    appMenu.onKey(appMenu.getListView(), KeyEvent.KEYCODE_MENU, down);
                    appMenu.onKey(appMenu.getListView(), KeyEvent.KEYCODE_MENU, up);
                });
        mMenuObserver.menuHiddenCallback.waitForCallback(0);
    }

    @Test
    @MediumTest
    public void testAppMenuKeyEvent_IgnoreUnrelatedKeyCode() throws Exception {
        showMenuAndAssert();

        AppMenu appMenu = mAppMenuHandler.getAppMenu();
        KeyEvent unrelated = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BOOKMARK);
        Assert.assertFalse(
                "#onKeyEvent should return false for unrelated codes",
                appMenu.onKey(null, KeyEvent.KEYCODE_BOOKMARK, unrelated));
    }

    @Test
    @MediumTest
    public void testAppMenuKeyEvent_IgnoreUnrelatedKeyEvent() throws Exception {
        showMenuAndAssert();

        AppMenu appMenu = mAppMenuHandler.getAppMenu();
        KeyEvent unrelated = new KeyEvent(KeyEvent.ACTION_MULTIPLE, KeyEvent.KEYCODE_MENU);
        Assert.assertFalse(
                "#onKeyEvent should return false for unrelated events",
                appMenu.onKey(null, KeyEvent.KEYCODE_MENU, unrelated));
    }

    @Test
    @MediumTest
    public void testAppMenuKeyEvent_IgnoreEventsWhenHidden() throws Exception {
        // Show app menu to initialize, then hide.
        showMenuAndAssert();
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuHandler.hideAppMenu());
        mMenuObserver.menuHiddenCallback.waitForCallback(0);

        AppMenu appMenu = mAppMenuHandler.getAppMenu();
        Assert.assertNull("ListView should be null.", appMenu.getListView());
        KeyEvent down = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU);
        Assert.assertFalse(
                "#onKeyEvent should return false when app menu hidden",
                appMenu.onKey(null, KeyEvent.KEYCODE_MENU, null));
    }

    @Test
    @MediumTest
    @DisableIf.Build(message = "Flaky crbug.com/1494912", sdk_is_greater_than = VERSION_CODES.Q)
    public void testAppMenuButtonHelper_DownUp() throws Exception {
        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();

        Assert.assertFalse(
                "View should start unpressed",
                mTestMenuButtonDelegate.getMenuButtonView().isPressed());
        Assert.assertFalse("App menu should be not be active", buttonHelper.isAppMenuActive());

        MotionEvent downMotionEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), downMotionEvent);

        waitForMenuToShow(0);
        Assert.assertTrue("Menu should be showing", mAppMenuHandler.isAppMenuShowing());
        Assert.assertTrue(
                "View should be pressed", mTestMenuButtonDelegate.getMenuButtonView().isPressed());
        Assert.assertTrue("App menu should be active", buttonHelper.isAppMenuActive());

        MotionEvent upMotionEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), upMotionEvent);

        Assert.assertFalse(
                "View should no longer be pressed",
                mTestMenuButtonDelegate.getMenuButtonView().isPressed());
        Assert.assertTrue("App menu should still be active", buttonHelper.isAppMenuActive());
    }

    @Test
    @MediumTest
    @DisableIf.Build(
            sdk_is_greater_than = VERSION_CODES.Q,
            message = "Flaky. See crbug.com/41496891")
    public void testAppMenuButtonHelper_DownCancel() throws Exception {
        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();
        Assert.assertFalse(
                "View should start unpressed",
                mTestMenuButtonDelegate.getMenuButtonView().isPressed());

        MotionEvent downMotionEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), downMotionEvent);

        waitForMenuToShow(0);
        Assert.assertTrue("Menu should be showing", mAppMenuHandler.isAppMenuShowing());

        Assert.assertTrue(
                "View should be pressed", mTestMenuButtonDelegate.getMenuButtonView().isPressed());

        MotionEvent cancelMotionEvent =
                MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), cancelMotionEvent);

        Assert.assertFalse(
                "View should no longer be pressed",
                mTestMenuButtonDelegate.getMenuButtonView().isPressed());
    }

    @Test
    @MediumTest
    public void testAppMenuButtonHelper_ClickRunnable() throws Exception {
        Assert.assertFalse(
                "View should start unpressed",
                mTestMenuButtonDelegate.getMenuButtonView().isPressed());

        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();
        CallbackHelper clickCallbackHelper = new CallbackHelper();
        Runnable clickRunnable = () -> clickCallbackHelper.notifyCalled();
        buttonHelper.setOnClickRunnable(clickRunnable);

        MotionEvent downMotionEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), downMotionEvent);

        clickCallbackHelper.waitForCallback(0);
        waitForMenuToShow(0);
    }

    @Test
    @MediumTest
    public void testAppMenuButtonHelper_ShowTwice() throws Exception {
        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();

        CallbackHelper showCallbackHelper = new CallbackHelper();
        Runnable showListener = () -> showCallbackHelper.notifyCalled();
        buttonHelper.setOnAppMenuShownListener(showListener);

        MotionEvent downMotionEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), downMotionEvent);

        waitForMenuToShow(0);
        Assert.assertTrue("Menu should be showing", mAppMenuHandler.isAppMenuShowing());
        Assert.assertEquals(
                "Runnable should have been called once", 1, showCallbackHelper.getCallCount());

        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), downMotionEvent);

        Assert.assertEquals(
                "Runnable should still only have been called once",
                1,
                showCallbackHelper.getCallCount());
    }

    @Test
    @MediumTest
    public void testAppMenuButtonHelper_ShowBlocked() throws Exception {
        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();
        AppMenuBlocker blocker1 = () -> false;
        mAppMenuCoordinator.registerAppMenuBlocker(blocker1);

        CallbackHelper showCallbackHelper = new CallbackHelper();
        Runnable showListener = () -> showCallbackHelper.notifyCalled();
        buttonHelper.setOnAppMenuShownListener(showListener);

        MotionEvent downMotionEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), downMotionEvent);

        Assert.assertEquals(
                "Runnable should not have been called once", 0, showCallbackHelper.getCallCount());
    }

    @Test
    @MediumTest
    public void testAppMenuButtonHelper_AccessibilityActions() throws Exception {
        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();

        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        buttonHelper.performAccessibilityAction(
                                mTestMenuButtonDelegate.getMenuButtonView(),
                                AccessibilityNodeInfo.ACTION_CLICK,
                                null));

        waitForMenuToShow(0);
        Assert.assertTrue("Menu should be showing", mAppMenuHandler.isAppMenuShowing());

        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        buttonHelper.performAccessibilityAction(
                                mTestMenuButtonDelegate.getMenuButtonView(),
                                AccessibilityNodeInfo.ACTION_CLICK,
                                null));

        mMenuObserver.menuHiddenCallback.waitForCallback(0);
        Assert.assertFalse("Menu should be hidden", mAppMenuHandler.isAppMenuShowing());
    }

    @Test
    @MediumTest
    public void testAppMenuButtonHelper_showEnterKeyPress() throws Exception {
        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();

        ThreadUtils.runOnUiThreadBlocking(
                () -> buttonHelper.onEnterKeyPress(mTestMenuButtonDelegate.getMenuButtonView()));

        waitForMenuToShow(0);
        Assert.assertTrue("Menu should be showing", mAppMenuHandler.isAppMenuShowing());
    }

    @Test
    @MediumTest
    @DisableIf.Device(type = {UiDisableIf.TABLET})
    @DisabledTest(message = "crbug.com/1186468")
    public void testDragHelper_ClickItem() throws Exception {
        AppMenuButtonHelperImpl buttonHelper =
                (AppMenuButtonHelperImpl) mAppMenuHandler.createAppMenuButtonHelper();

        Assert.assertFalse(
                "View should start unpressed",
                mTestMenuButtonDelegate.getMenuButtonView().isPressed());
        Assert.assertFalse("App menu should be not be active", buttonHelper.isAppMenuActive());

        MotionEvent downMotionEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), downMotionEvent);

        waitForMenuToShow(0);
        CriteriaHelper.pollUiThread(
                () -> mAppMenuHandler.getAppMenuDragHelper().isReadyForMenuItemAction());

        Rect firstItemScreenRect = getVisibleScreenRectAtPosition(0);
        int eventX = firstItemScreenRect.left + (firstItemScreenRect.width() / 2);
        int eventY = firstItemScreenRect.top + (firstItemScreenRect.height() / 2);

        MotionEvent dragMotionEvent =
                MotionEvent.obtain(0, 100, MotionEvent.ACTION_MOVE, eventX, eventY, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), dragMotionEvent);

        MotionEvent upMotionEvent =
                MotionEvent.obtain(0, 150, MotionEvent.ACTION_UP, eventX, eventY, 0);
        sendMotionEventToButtonHelper(
                buttonHelper, mTestMenuButtonDelegate.getMenuButtonView(), upMotionEvent);
        mDelegate.itemSelectedCallbackHelper.waitForCallback(
                "itemRect: " + firstItemScreenRect + " eventX: " + eventX + " eventY: " + eventY,
                0);
        Assert.assertEquals(
                "Incorrect id for last selected item.",
                R.id.menu_item_one,
                mDelegate.lastSelectedItemId);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_enoughSpace() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ -1,
                                /* availableScreenSpace= */ 35);
        Assert.assertEquals(30, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_notEnoughSpaceForOneItem() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ -1,
                                /* availableScreenSpace= */ 26);
        // The space only can fit the 1st and 2nd items and the partial 3rd item.
        Assert.assertEquals(25, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_notEnoughSpaceForTwoItem() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ -1,
                                /* availableScreenSpace= */ 24);
        // The space only can fit the full 1st item, the full 2nd items and the partial 3rd item.
        // The space for the 3rd item is 4, but since the menu is small enough, we show the maximum
        // available height instead of switching to the partial 3rd item.
        Assert.assertEquals(24, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_notEnoughSpaceForThreeItem() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 3, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ -1,
                                /* availableScreenSpace= */ 34);
        // The space only can fit the full 1st item, the full 2nd item, the full 3rd item, and the
        // partial 4th item. But the space for 4th item is 4, which is not enough to show partial
        // 3rd item(5 = LAST_ITEM_SHOW_FRACTION * 10), we show the partial 3rd item instead.
        Assert.assertEquals(25, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_notEnoughSpaceForDivider() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 3, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 4, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ 3,
                                /* availableScreenSpace= */ 36);
        // The space only can fit the 1st, 2nd, 3rd, and partial 4th item. But the 4th item is a
        // divider line, so we show only the partial 3rd item.
        Assert.assertEquals(25, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_showPartialDivider() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 3, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ 2,
                                /* availableScreenSpace= */ 26);
        // The space only can fit the 1st, 2nd and the partial 3rd item. The third item
        // is a divider line, and the menu is small enough that we still want to use all available
        // space.
        Assert.assertEquals(26, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_notEnoughSpaceForItemShowPartialDivider()
            throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 3, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ 2,
                                /* availableScreenSpace= */ 34);
        // The space only can fit the full 1st, 2nd and 3rd item and the partial 4th item.
        // But the space for 4th item is 4, which is not enough to show partial 4th item(5 =
        // LAST_ITEM_SHOW_FRACTION * 10), so we should show the partial 3rd item instead. The third
        // item is a divider line, and the menu is small enough that we still want to use all
        // available space.
        Assert.assertEquals(34, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_minimalHight() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ -1,
                                /* availableScreenSpace= */ 4);
        // The space is not enough for any item, but we still show 1 and half items at least.
        Assert.assertEquals(15, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_minimalHight_notEnoughSpaceForDivider()
            throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();
        createMenuItem(menuItemIds, heightList, /* id= */ 0, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 1, /* height= */ 10);
        createMenuItem(menuItemIds, heightList, /* id= */ 2, /* height= */ 10);

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ 1,
                                /* availableScreenSpace= */ 6);
        // The space is not enough for any item, but we still show 1 and half items at least.
        Assert.assertEquals(15, height);
    }

    @Test
    @SmallTest
    public void testCalculateHeightForItems_nagativeSpaceForZeroItems() throws Exception {
        showMenuAndAssert();

        List<Integer> menuItemIds = new ArrayList<Integer>();
        List<Integer> heightList = new ArrayList<Integer>();

        int height =
                mAppMenuHandler
                        .getAppMenu()
                        .calculateHeightForItems(
                                menuItemIds,
                                heightList,
                                /* groupDividerResourceId= */ 1,
                                /* availableScreenSpace= */ -1);
        // Make sure there are no crashes.
        Assert.assertEquals(0, height);
    }

    private void createMenuItem(
            List<Integer> menuItemIds, List<Integer> heightList, int id, int height) {
        menuItemIds.add(id);
        heightList.add(height);
    }

    private void showMenuAndAssert() throws TimeoutException {
        int currentCallCount = mMenuObserver.menuShownCallback.getCallCount();
        ThreadUtils.runOnUiThreadBlocking(() -> mAppMenuCoordinator.showAppMenuForKeyboardEvent());
        waitForMenuToShow(currentCallCount);
    }

    private void waitForMenuToShow(int currentCallCount) throws TimeoutException {
        mMenuObserver.menuShownCallback.waitForCallback(currentCallCount);
        Assert.assertTrue("Menu should be showing", mAppMenuHandler.isAppMenuShowing());

        ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.getAppMenu().finishAnimationsForTests());
    }

    private class TestActivityLifecycleDispatcher implements ActivityLifecycleDispatcher {
        public CallbackHelper observerRegisteredCallbackHelper = new CallbackHelper();

        @Override
        public void register(LifecycleObserver observer) {
            observerRegisteredCallbackHelper.notifyCalled();
        }

        @Override
        public void unregister(LifecycleObserver observer) {}

        @Override
        public int getCurrentActivityState() {
            return 0;
        }

        @Override
        public boolean isNativeInitializationFinished() {
            return false;
        }

        @Override
        public boolean isActivityFinishingOrDestroyed() {
            return false;
        }
    }

    private class TestMenuButtonDelegate implements MenuButtonDelegate {
        @Nullable
        @Override
        public View getMenuButtonView() {
            return getActivity().findViewById(R.id.top_button);
        }
    }

    private View getViewAtPosition(int index) {
        // Wait for the view to be available. This is necessary when the menu is first shown.
        CriteriaHelper.pollUiThread(
                () ->
                        AppMenuTestSupport.getListView(mAppMenuCoordinator).getChildAt(index)
                                != null);
        return AppMenuTestSupport.getListView(mAppMenuCoordinator).getChildAt(index);
    }

    private Rect getPopupLocationRect() {
        View contentView = mAppMenuHandler.getAppMenu().getPopup().getContentView();

        Rect popupRect = new Rect();
        int[] popupLocation = new int[2];
        contentView.getLocationOnScreen(popupLocation);
        popupRect.left = popupLocation[0];
        popupRect.top = popupLocation[1];
        popupRect.right = popupLocation[0] + contentView.getWidth();
        popupRect.bottom = popupLocation[1] + contentView.getHeight();
        return popupRect;
    }

    private Rect getViewLocationRect(View anchor) {
        Rect viewRect = new Rect();
        int[] viewLocation = new int[2];
        anchor.getLocationOnScreen(viewLocation);
        viewRect.left = viewLocation[0];
        viewRect.top = viewLocation[1];
        viewRect.right = viewRect.left + anchor.getWidth();
        viewRect.bottom = viewRect.top + anchor.getHeight();
        return viewRect;
    }

    private Rect getVisibleScreenRectAtPosition(int position) throws ExecutionException {
        View view = getViewAtPosition(position);
        return ThreadUtils.runOnUiThreadBlocking(
                () -> mAppMenuHandler.getAppMenuDragHelper().getScreenVisibleRect(view));
    }

    private void sendMotionEventToButtonHelper(
            AppMenuButtonHelperImpl helper, View view, MotionEvent event)
            throws ExecutionException {
        ThreadUtils.runOnUiThreadBlocking(() -> helper.onTouch(view, event));
    }

    private void checkHighlightOn(View view) {
        Assert.assertTrue(ViewHighlighterTestUtils.checkHighlightOn(view));
    }
}