chromium/chrome/android/javatests/src/org/chromium/chrome/browser/contextmenu/ContextMenuTest.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.contextmenu;

import static androidx.test.espresso.matcher.ViewMatchers.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.chromium.chrome.browser.contextmenu.ContextMenuCoordinator.ListItemType.CONTEXT_MENU_ITEM;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Looper;
import android.text.TextUtils;
import android.view.KeyEvent;

import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
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.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import org.chromium.base.Callback;
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.CloseableOnMainThread;
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.DisableIf;
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.RequiresRestart;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.download.DownloadTestRule;
import org.chromium.chrome.browser.download.DownloadTestRule.CustomMainActivityStart;
import org.chromium.chrome.browser.ephemeraltab.EphemeralTabCoordinator;
import org.chromium.chrome.browser.firstrun.FirstRunStatus;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.share.ChromeShareExtras;
import org.chromium.chrome.browser.share.LensUtils;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.share.ShareDelegate.ShareOrigin;
import org.chromium.chrome.browser.share.ShareDelegateSupplier;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabContextMenuItemDelegate;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.browser.contextmenu.ContextMenuUtils;
import org.chromium.components.browser_ui.share.ShareParams;
import org.chromium.components.embedder_support.contextmenu.ChipDelegate;
import org.chromium.components.embedder_support.contextmenu.ChipRenderParams;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.components.embedder_support.contextmenu.ContextMenuPopulatorFactory;
import org.chromium.components.externalauth.ExternalAuthUtils;
import org.chromium.components.policy.test.annotations.Policies;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.content_public.browser.test.util.TestTouchUtils;
import org.chromium.content_public.common.ContentFeatures;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.ui.base.Clipboard;
import org.chromium.ui.base.MenuSourceType;
import org.chromium.ui.test.util.UiDisableIf;
import org.chromium.url.GURL;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

/** Instrumentation tests for the context menu. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    ChromeSwitches.GOOGLE_BASE_URL + "=http://example.com/"
})
@Batch(Batch.PER_CLASS)
public class ContextMenuTest {

    @Mock private TabContextMenuItemDelegate mItemDelegate;
    @Mock private ShareDelegate mShareDelegate;

    @ClassRule
    public static DownloadTestRule sDownloadTestRule =
            new DownloadTestRule(
                    new CustomMainActivityStart() {
                        @Override
                        public void customMainActivityStart() throws InterruptedException {
                            sDownloadTestRule.startMainActivityOnBlankPage();
                        }
                    });

    @Rule
    public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
            new BlankCTATabInitialStateRule(sDownloadTestRule, false);

    private static final String TEST_PATH =
            "/chrome/test/data/android/contextmenu/context_menu_test.html";

    private EmbeddedTestServer mTestServer;
    private String mTestUrl;

    private ContextMenuCoordinator mMenuCoordinator;

    private static final String FILENAME_GIF = "download.gif";
    private static final String FILENAME_PNG = "test_image.png";
    private static final String FILENAME_WEBM = "test.webm";
    private static final String TEST_GIF_IMAGE_FILE_EXTENSION = ".gif";
    private static final String TEST_JPG_IMAGE_FILE_EXTENSION = ".jpg";

    // Test chip delegate that always returns valid chip render params.
    private static final ChipDelegate FAKE_CHIP_DELEGATE =
            new ChipDelegate() {
                @Override
                public boolean isChipSupported() {
                    return true;
                }

                @Override
                public void getChipRenderParams(Callback<ChipRenderParams> callback) {
                    // Do nothing.
                }

                @Override
                public void onMenuClosed() {
                    // Do nothing.
                }

                @Override
                public boolean isValidChipRenderParams(ChipRenderParams chipRenderParams) {
                    return true;
                }
            };

    // Test Lens chip delegate that always returns valid chip render params.
    private void setupLensChipDelegate() {
        LensChipDelegate.setShouldSkipIsEnabledCheckForTesting(true);
    }

    private static final String[] TEST_FILES =
            new String[] {FILENAME_GIF, FILENAME_PNG, FILENAME_WEBM};

    @BeforeClass
    public static void beforeClass() {
        Looper.prepare();
    }

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(true));

        mTestServer = sDownloadTestRule.getTestServer();
        mTestUrl = mTestServer.getURL(TEST_PATH);
        deleteTestFiles();
        sDownloadTestRule.loadUrl(mTestUrl);
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        CriteriaHelper.pollUiThread(() -> tab.isUserInteractable() && !tab.isLoading());
        setupLensChipDelegate();
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(() -> FirstRunStatus.setFirstRunFlowComplete(false));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    if (mMenuCoordinator != null) {
                        mMenuCoordinator.dismiss();
                        mMenuCoordinator = null;
                    }
                });
    }

    @Test
    @MediumTest
    public void testCopyLinkURL() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        // Allow DiskWrites temporarily in main thread to avoid
        // violation during copying under emulator environment.
        try (CloseableOnMainThread ignored = CloseableOnMainThread.StrictMode.allowDiskWrites()) {
            ContextMenuUtils.selectContextMenuItem(
                    InstrumentationRegistry.getInstrumentation(),
                    sDownloadTestRule.getActivity(),
                    tab,
                    "testLink",
                    R.id.contextmenu_copy_link_address);
        }

        assertStringContains("test_link.html", getClipboardText());
    }

    @Test
    @MediumTest
    @Feature({"Browser"})
    public void testCopyImageLinkCopiesLinkURL() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        // Allow DiskWrites temporarily in main thread to avoid
        // violation during copying under emulator environment.
        try (CloseableOnMainThread ignored = CloseableOnMainThread.StrictMode.allowDiskWrites()) {
            ContextMenuUtils.selectContextMenuItem(
                    InstrumentationRegistry.getInstrumentation(),
                    sDownloadTestRule.getActivity(),
                    tab,
                    "testImageLink",
                    R.id.contextmenu_copy_link_address);
        }

        assertStringContains("test_link.html", getClipboardText());
    }

    @Test
    @MediumTest
    @Feature({"Browser"})
    @RequiresRestart
    public void testLongPressOnImage() throws TimeoutException {
        checkOpenImageInNewTab("testImage", "/chrome/test/data/android/contextmenu/test_image.png");
    }

    @Test
    @MediumTest
    @Feature({"Browser"})
    public void testLongPressOnImageLink() throws TimeoutException {
        checkOpenImageInNewTab(
                "testImageLink", "/chrome/test/data/android/contextmenu/test_image.png");
    }

    private void checkOpenImageInNewTab(String domId, final String expectedPath)
            throws TimeoutException {
        final Tab activityTab = sDownloadTestRule.getActivity().getActivityTab();

        final CallbackHelper newTabCallback = new CallbackHelper();
        final AtomicReference<Tab> newTab = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    sDownloadTestRule
                            .getActivity()
                            .getTabModelSelector()
                            .addObserver(
                                    new TabModelSelectorObserver() {
                                        @Override
                                        public void onNewTabCreated(
                                                Tab tab, @TabCreationState int creationState) {
                                            if (tab.getParentId() != activityTab.getId()) {
                                                return;
                                            }
                                            newTab.set(tab);
                                            newTabCallback.notifyCalled();

                                            sDownloadTestRule
                                                    .getActivity()
                                                    .getTabModelSelector()
                                                    .removeObserver(this);
                                        }
                                    });
                });

        int callbackCount = newTabCallback.getCallCount();

        ContextMenuUtils.selectContextMenuItem(
                InstrumentationRegistry.getInstrumentation(),
                sDownloadTestRule.getActivity(),
                activityTab,
                domId,
                R.id.contextmenu_open_image_in_new_tab);

        try {
            newTabCallback.waitForCallback(callbackCount);
        } catch (TimeoutException ex) {
            Assert.fail("New tab never created from context menu press");
        }

        // Only check for the URL matching as the tab will not be fully created in svelte mode.
        final String expectedUrl = mTestServer.getURL(expectedPath);
        CriteriaHelper.pollUiThread(
                () ->
                        Criteria.checkThat(
                                ChromeTabUtils.getUrlStringOnUiThread(newTab.get()),
                                Matchers.is(expectedUrl)));
    }

    @Test
    @MediumTest
    public void testDismissContextMenuOnBack() throws TimeoutException {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        Assert.assertNotNull("Context menu was not properly created", mMenuCoordinator);
        CriteriaHelper.pollUiThread(
                () -> {
                    return !sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Context menu did not have window focus");

        InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
        CriteriaHelper.pollUiThread(
                () -> {
                    return sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Activity did not regain focus.");
    }

    @Test
    @MediumTest
    @Feature({"Browser"})
    @EnableFeatures({ChromeFeatureList.CONTEXT_MENU_TRANSLATE_WITH_GOOGLE_LENS})
    @DisableFeatures({ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU})
    public void testLensTranslateChipNotShowingIfNotEnabled() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        hardcodeTestImageForSharing(TEST_JPG_IMAGE_FILE_EXTENSION);

        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        // Needs to run on UI thread so creation happens on same thread as dismissal.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertNull(
                            "Chip popoup was initialized.",
                            mMenuCoordinator.getCurrentPopupWindowForTesting());
                });
    }

    @Test
    @MediumTest
    @Feature({"Browser"})
    @DisableFeatures({ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU})
    public void testSelectLensTranslateChip() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        hardcodeTestImageForSharing(TEST_JPG_IMAGE_FILE_EXTENSION);

        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        // Needs to run on UI thread so creation happens on same thread as dismissal.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mMenuCoordinator.simulateTranslateImageClassificationForTesting();
                    Assert.assertTrue(
                            "Chip popoup not showing.",
                            mMenuCoordinator.getCurrentPopupWindowForTesting().isShowing());
                    mMenuCoordinator.clickChipForTesting();
                });

        Assert.assertFalse(
                "Chip popoup still showing.",
                mMenuCoordinator.getCurrentPopupWindowForTesting().isShowing());
    }

    @Test
    @MediumTest
    @Feature({"Browser"})
    @EnableFeatures({ChromeFeatureList.CONTEXT_MENU_TRANSLATE_WITH_GOOGLE_LENS})
    @DisableFeatures({ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU})
    public void testLensChipNotShowingAfterMenuDismissed() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        hardcodeTestImageForSharing(TEST_JPG_IMAGE_FILE_EXTENSION);

        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        // Dismiss context menu.
        TestTouchUtils.singleClickView(
                InstrumentationRegistry.getInstrumentation(),
                tab.getView(),
                tab.getView().getWidth() - 5,
                tab.getView().getHeight() - 5);
        // Needs to run on UI thread so creation happens on same thread as dismissal.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ChipRenderParams chipRenderParams =
                            mMenuCoordinator.simulateImageClassificationForTesting();
                    mMenuCoordinator
                            .getChipRenderParamsCallbackForTesting(FAKE_CHIP_DELEGATE)
                            .bind(chipRenderParams)
                            .run();
                    Assert.assertNull(
                            "Chip popoup was initialized.",
                            mMenuCoordinator.getCurrentPopupWindowForTesting());
                });
    }

    // Assert that focus is unchanged and that the chip popup does not block the dismissal of the
    // context menu.
    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.CONTEXT_MENU_TRANSLATE_WITH_GOOGLE_LENS})
    @DisableFeatures({ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU})
    public void testDismissContextMenuOnClickLensTranslateChipEnabled() throws TimeoutException {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        // Needs to run on UI thread so creation happens on same thread as dismissal.
        ThreadUtils.runOnUiThreadBlocking(
                () -> mMenuCoordinator.simulateTranslateImageClassificationForTesting());
        Assert.assertNotNull("Context menu was not properly created", mMenuCoordinator);
        CriteriaHelper.pollUiThread(
                () -> {
                    return !sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Context menu did not have window focus");

        TestTouchUtils.singleClickView(
                InstrumentationRegistry.getInstrumentation(),
                tab.getView(),
                tab.getView().getWidth() - 5,
                tab.getView().getHeight() - 5);

        CriteriaHelper.pollUiThread(
                () -> {
                    return sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Activity did not regain focus.");
    }

    @Test
    @MediumTest
    @Feature({"Browser"})
    @DisableFeatures({ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU})
    public void testSelectLensShoppingChip() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        hardcodeTestImageForSharing(TEST_JPG_IMAGE_FILE_EXTENSION);

        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        // Needs to run on UI thread so creation happens on same thread as dismissal.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mMenuCoordinator.simulateShoppyImageClassificationForTesting();
                    Assert.assertTrue(
                            "Chip popoup not showing.",
                            mMenuCoordinator.getCurrentPopupWindowForTesting().isShowing());
                    mMenuCoordinator.clickChipForTesting();
                });

        Assert.assertFalse(
                "Chip popoup still showing.",
                mMenuCoordinator.getCurrentPopupWindowForTesting().isShowing());
    }

    // Assert that focus is unchanged and that the chip popup does not block the dismissal of the
    // context menu.
    @Test
    @MediumTest
    @DisableFeatures({ContentFeatures.TOUCH_DRAG_AND_CONTEXT_MENU})
    public void testDismissContextMenuOnClickShoppingLensChipEnabled() throws TimeoutException {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        // Needs to run on UI thread so creation happens on same thread as dismissal.
        ThreadUtils.runOnUiThreadBlocking(
                () -> mMenuCoordinator.simulateShoppyImageClassificationForTesting());
        Assert.assertNotNull("Context menu was not properly created", mMenuCoordinator);
        CriteriaHelper.pollUiThread(
                () -> {
                    return !sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Context menu did not have window focus");

        TestTouchUtils.singleClickView(
                InstrumentationRegistry.getInstrumentation(),
                tab.getView(),
                tab.getView().getWidth() - 5,
                tab.getView().getHeight() - 5);

        CriteriaHelper.pollUiThread(
                () -> {
                    return sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Activity did not regain focus.");
    }

    @Test
    @MediumTest
    public void testDismissContextMenuOnClick() throws TimeoutException {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        Assert.assertNotNull("Context menu was not properly created", mMenuCoordinator);
        CriteriaHelper.pollUiThread(
                () -> {
                    return !sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Context menu did not have window focus");

        TestTouchUtils.singleClickView(
                InstrumentationRegistry.getInstrumentation(),
                tab.getView(),
                tab.getView().getWidth() - 5,
                tab.getView().getHeight() - 5);

        CriteriaHelper.pollUiThread(
                () -> {
                    return sDownloadTestRule.getActivity().hasWindowFocus();
                },
                "Activity did not regain focus.");
    }

    @Test
    @MediumTest
    public void testCopyEmailAddress() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        // Allow all thread policies temporarily in main thread to avoid
        // DiskWrite and UnBufferedIo violations during copying under
        // emulator environment.
        try (CloseableOnMainThread ignored =
                CloseableOnMainThread.StrictMode.allowAllThreadPolicies()) {
            ContextMenuUtils.selectContextMenuItem(
                    InstrumentationRegistry.getInstrumentation(),
                    sDownloadTestRule.getActivity(),
                    tab,
                    "testEmail",
                    R.id.contextmenu_copy);
        }

        Assert.assertEquals(
                "Copied email address is not correct",
                "[email protected],[email protected]",
                getClipboardText());
    }

    @Test
    @MediumTest
    public void testCopyTelNumber() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        // Allow DiskWrites temporarily in main thread to avoid
        // violation during copying under emulator environment.
        try (CloseableOnMainThread ignored = CloseableOnMainThread.StrictMode.allowDiskWrites()) {
            ContextMenuUtils.selectContextMenuItem(
                    InstrumentationRegistry.getInstrumentation(),
                    sDownloadTestRule.getActivity(),
                    tab,
                    "testTel",
                    R.id.contextmenu_copy);
        }

        Assert.assertEquals("Copied tel number is not correct", "10000000000", getClipboardText());
    }

    @Test
    @LargeTest
    public void testSaveDataUrl() throws TimeoutException, SecurityException, IOException {
        saveMediaFromContextMenu("dataUrlIcon", R.id.contextmenu_save_image, FILENAME_GIF);
    }

    @Test
    @LargeTest
    public void testSaveImage() throws TimeoutException, SecurityException, IOException {
        saveMediaFromContextMenu("testImage", R.id.contextmenu_save_image, FILENAME_PNG);
    }

    @Test
    @LargeTest
    public void testSaveVideo() throws TimeoutException, SecurityException, IOException {
        saveMediaFromContextMenu("videoDOMElement", R.id.contextmenu_save_video, FILENAME_WEBM);
    }

    /**
     * Opens a link and image in new tabs and verifies the order of the tabs. Also verifies that the
     * parent page remains in front after opening links in new tabs.
     *
     * <p>This test only applies in tabbed mode. In document mode, Android handles the ordering of
     * the tabs.
     */
    @Test
    @LargeTest
    public void testOpenLinksInNewTabsAndVerifyTabIndexOrdering() throws TimeoutException {
        TabModel tabModel = sDownloadTestRule.getActivity().getCurrentTabModel();
        int numOpenedTabs = tabModel.getCount();
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        ContextMenuUtils.selectContextMenuItem(
                InstrumentationRegistry.getInstrumentation(),
                sDownloadTestRule.getActivity(),
                tab,
                "testLink",
                R.id.contextmenu_open_in_new_tab);
        int indexOfLinkPage = numOpenedTabs;
        final int expectedNumOpenedTabs = indexOfLinkPage + 1;
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Number of open tabs does not match",
                            tabModel.getCount(),
                            Matchers.is(expectedNumOpenedTabs));
                });
        numOpenedTabs = expectedNumOpenedTabs;

        // Wait for any new tab animation to finish if we're being driven by the compositor.
        final LayoutManagerImpl layoutDriver =
                sDownloadTestRule
                        .getActivity()
                        .getCompositorViewHolderForTesting()
                        .getLayoutManager();
        CriteriaHelper.pollUiThread(
                () -> {
                    return layoutDriver.getActiveLayout().shouldDisplayContentOverlay();
                },
                "Background tab animation not finished.");

        ContextMenuUtils.selectContextMenuItem(
                InstrumentationRegistry.getInstrumentation(),
                sDownloadTestRule.getActivity(),
                tab,
                "testLink2",
                R.id.contextmenu_open_in_new_tab);
        int indexOfLinkPage2 = numOpenedTabs;
        final int expectedNumOpenedTabs2 = indexOfLinkPage2 + 1;
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            "Number of open tabs does not match",
                            tabModel.getCount(),
                            Matchers.is(expectedNumOpenedTabs2));
                });
        numOpenedTabs = expectedNumOpenedTabs2;

        // Verify the Url is still the same of Parent page.
        Assert.assertEquals(
                mTestUrl,
                ChromeTabUtils.getUrlStringOnUiThread(
                        sDownloadTestRule.getActivity().getActivityTab()));

        // Verify that the background tabs were opened in the expected order.
        String newTabUrl =
                mTestServer.getURL("/chrome/test/data/android/contextmenu/test_link.html");
        Assert.assertEquals(
                newTabUrl,
                ChromeTabUtils.getUrlStringOnUiThread(tabModel.getTabAt(indexOfLinkPage)));

        String imageUrl =
                mTestServer.getURL("/chrome/test/data/android/contextmenu/test_link2.html");
        Assert.assertEquals(
                imageUrl,
                ChromeTabUtils.getUrlStringOnUiThread(tabModel.getTabAt(indexOfLinkPage2)));
    }

    @Test
    @SmallTest
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338969612
    @Feature({"Browser", "ContextMenu"})
    public void testContextMenuRetrievesLinkOptions() throws TimeoutException {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testLink");

        Integer[] expectedItems = {
            R.id.contextmenu_open_in_new_tab_in_group,
            R.id.contextmenu_open_in_new_tab,
            R.id.contextmenu_open_in_incognito_tab,
            R.id.contextmenu_save_link_as,
            R.id.contextmenu_copy_link_text,
            R.id.contextmenu_copy_link_address,
            R.id.contextmenu_share_link,
            R.id.contextmenu_read_later
        };
        expectedItems =
                addItemsIf(
                        EphemeralTabCoordinator.isSupported(),
                        expectedItems,
                        new Integer[] {R.id.contextmenu_open_in_ephemeral_tab});
        assertMenuItemsAreEqual(mMenuCoordinator, expectedItems);
    }

    @Test
    @SmallTest
    @Feature({"Browser", "ContextMenu"})
    @RequiresRestart
    public void testContextMenuRetrievesImageOptions() throws TimeoutException {
        LensUtils.setFakePassableLensEnvironmentForTesting(true);

        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");

        Integer[] expectedItems = {
            R.id.contextmenu_save_image,
            R.id.contextmenu_open_image_in_new_tab,
            R.id.contextmenu_search_with_google_lens,
            R.id.contextmenu_share_image,
            R.id.contextmenu_copy_image
        };
        Integer[] featureItems = {R.id.contextmenu_open_image_in_ephemeral_tab};
        expectedItems =
                addItemsIf(EphemeralTabCoordinator.isSupported(), expectedItems, featureItems);
        assertMenuItemsAreEqual(mMenuCoordinator, expectedItems);
    }

    @Test
    @SmallTest
    @Feature({"Browser", "ContextMenu"})
    @Policies.Add({@Policies.Item(key = "DefaultSearchProviderEnabled", string = "false")})
    public void testContextMenuRetrievesImageOptions_NoDefaultSearchEngine()
            throws TimeoutException {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");

        Integer[] expectedItems = {
            R.id.contextmenu_save_image,
            R.id.contextmenu_open_image_in_new_tab,
            R.id.contextmenu_share_image,
            R.id.contextmenu_copy_image
        };
        Integer[] featureItems = {R.id.contextmenu_open_image_in_ephemeral_tab};
        expectedItems =
                addItemsIf(EphemeralTabCoordinator.isSupported(), expectedItems, featureItems);
        assertMenuItemsAreEqual(mMenuCoordinator, expectedItems);
    }

    @Test
    @SmallTest
    @Feature({"Browser", "ContextMenu"})
    @Policies.Add({@Policies.Item(key = "DefaultSearchProviderEnabled", string = "false")})
    public void testContextMenuRetrievesImageOptions_NoDefaultSearchEngineLensEnabled()
            throws TimeoutException {
        LensUtils.setFakePassableLensEnvironmentForTesting(true);

        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");

        // Search with Google Lens is only supported when Google is the default search provider.
        Integer[] expectedItems = {
            R.id.contextmenu_save_image,
            R.id.contextmenu_open_image_in_new_tab,
            R.id.contextmenu_share_image,
            R.id.contextmenu_copy_image
        };
        Integer[] featureItems = {R.id.contextmenu_open_image_in_ephemeral_tab};
        expectedItems =
                addItemsIf(EphemeralTabCoordinator.isSupported(), expectedItems, featureItems);
        assertMenuItemsAreEqual(mMenuCoordinator, expectedItems);
    }

    @Test
    @SmallTest
    @DisableIf.Device(type = {UiDisableIf.TABLET}) // https://crbug.com/338969612
    @Feature({"Browser", "ContextMenu"})
    public void testContextMenuRetrievesImageLinkOptions() throws TimeoutException {
        LensUtils.setFakePassableLensEnvironmentForTesting(true);

        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImageLink");

        Integer[] expectedItems = {
            R.id.contextmenu_open_in_new_tab_in_group,
            R.id.contextmenu_open_in_new_tab,
            R.id.contextmenu_open_in_incognito_tab,
            R.id.contextmenu_copy_link_address,
            R.id.contextmenu_save_link_as,
            R.id.contextmenu_save_image,
            R.id.contextmenu_open_image_in_new_tab,
            R.id.contextmenu_search_with_google_lens,
            R.id.contextmenu_share_image,
            R.id.contextmenu_share_link,
            R.id.contextmenu_copy_image
        };
        Integer[] featureItems = {
            R.id.contextmenu_open_in_ephemeral_tab, R.id.contextmenu_open_image_in_ephemeral_tab
        };
        expectedItems =
                addItemsIf(EphemeralTabCoordinator.isSupported(), expectedItems, featureItems);
        assertMenuItemsAreEqual(mMenuCoordinator, expectedItems);
    }

    @Test
    @SmallTest
    @Feature({"Browser", "ContextMenu"})
    public void testContextMenuRetrievesVideoOptions() throws TimeoutException {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        DOMUtils.clickNode(
                sDownloadTestRule.getActivity().getCurrentWebContents(), "videoDOMElement");
        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "videoDOMElement");

        Integer[] expectedItems = {R.id.contextmenu_save_video};
        assertMenuItemsAreEqual(mMenuCoordinator, expectedItems);
    }

    @Test
    @SmallTest
    @Feature({"Browser", "ContextMenu"})
    public void testSearchImageWithGoogleLensMenuItemName() throws Throwable {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();

        LensUtils.setFakePassableLensEnvironmentForTesting(true);
        hardcodeTestImageForSharing(TEST_JPG_IMAGE_FILE_EXTENSION);

        mMenuCoordinator = ContextMenuUtils.openContextMenu(tab, "testImage");
        Integer[] expectedItems = {
            R.id.contextmenu_save_image,
            R.id.contextmenu_open_image_in_new_tab,
            R.id.contextmenu_share_image,
            R.id.contextmenu_copy_image,
            R.id.contextmenu_search_with_google_lens
        };
        expectedItems =
                addItemsIf(
                        EphemeralTabCoordinator.isSupported(),
                        expectedItems,
                        new Integer[] {R.id.contextmenu_open_image_in_ephemeral_tab});
        String title =
                getMenuTitleFromItem(mMenuCoordinator, R.id.contextmenu_search_with_google_lens);
        Assert.assertTrue(
                "Context menu item name should be \'Search image with Google Lens\'.",
                title.startsWith("Search image with Google Lens"));
        assertMenuItemsAreEqual(mMenuCoordinator, expectedItems);
    }

    @Test
    @SmallTest
    @Feature({"Browser", "ContextMenu"})
    public void testCopyImage() throws Throwable {
        // Clear the clipboard.
        Clipboard.getInstance().setText("");

        hardcodeTestImageForSharing(TEST_GIF_IMAGE_FILE_EXTENSION);
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        // Allow all thread policies temporarily in main thread to avoid
        // DiskWrite and UnBufferedIo violations during copying under
        // emulator environment.
        try (CloseableOnMainThread ignored =
                CloseableOnMainThread.StrictMode.allowAllThreadPolicies()) {
            ContextMenuUtils.selectContextMenuItem(
                    InstrumentationRegistry.getInstrumentation(),
                    sDownloadTestRule.getActivity(),
                    tab,
                    "dataUrlIcon",
                    R.id.contextmenu_copy_image);
        }

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            Clipboard.getInstance().getImageUri(), Matchers.notNullValue());
                });

        String imageUriString = Clipboard.getInstance().getImageUri().toString();

        Assert.assertTrue(
                "Image content prefix is not correct",
                imageUriString.startsWith(
                        "content://org.chromium.chrome.tests.FileProvider/images/screenshot/"));
        Assert.assertTrue(
                "Image extension is not correct",
                imageUriString.endsWith(TEST_GIF_IMAGE_FILE_EXTENSION));

        // Clean up the clipboard.
        Clipboard.getInstance().setText("");
    }

    @Test
    @SmallTest
    @Feature({"Browser", "ContextMenu"})
    public void testContextMenuOpenedFromHighlight() {
        when(mItemDelegate.isIncognito()).thenReturn(false);
        when(mItemDelegate.getPageTitle()).thenReturn("");

        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        ContextMenuHelper contextMenuHelper =
                ContextMenuHelper.createForTesting(0, tab.getWebContents());
        ContextMenuParams params =
                new ContextMenuParams(
                        0,
                        0,
                        new GURL("http://example.com/"),
                        GURL.emptyGURL(),
                        "",
                        GURL.emptyGURL(),
                        GURL.emptyGURL(),
                        "",
                        null,
                        false,
                        0,
                        0,
                        MenuSourceType.MENU_SOURCE_TOUCH,
                        /* getOpenedFromHighlight= */ true,
                        /* additionalNavigationParams= */ null);
        ContextMenuPopulatorFactory populatorFactory =
                new ChromeContextMenuPopulatorFactory(
                        mItemDelegate,
                        () -> mShareDelegate,
                        ChromeContextMenuPopulator.ContextMenuMode.NORMAL,
                        ExternalAuthUtils.getInstance());
        Integer[] expectedItems = {
            R.id.contextmenu_share_highlight,
            R.id.contextmenu_remove_highlight,
            R.id.contextmenu_learn_more
        };
        var shown_histogram_watcher =
                HistogramWatcher.newSingleRecordWatcher("ContextMenu.Shown", 1);
        var shared_histogram_watcher =
                HistogramWatcher.newSingleRecordWatcher(
                        "ContextMenu.Shown.SharedHighlightingInteraction", 1);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ContextMenuHelper.setMenuShownCallbackForTests(
                            (coordinator) -> {
                                assertMenuItemsAreEqual(coordinator, expectedItems);
                                shown_histogram_watcher.assertExpected();
                                shared_histogram_watcher.assertExpected();
                            });
                    contextMenuHelper.showContextMenuForTesting(
                            populatorFactory, params, null, tab.getView(), 0);
                });
    }

    @Test
    @SmallTest
    @RequiresRestart
    public void testShareImage() throws Exception {
        hardcodeTestImageForSharing(TEST_JPG_IMAGE_FILE_EXTENSION);

        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        // Set share delegate before triggering context menu, so the mocked share delegate is used.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    var supplier =
                            (ShareDelegateSupplier)
                                    ShareDelegateSupplier.from(
                                            sDownloadTestRule.getActivity().getWindowAndroid());
                    Mockito.doReturn(true).when(mShareDelegate).isSharingHubEnabled();
                    supplier.set(mShareDelegate);
                });

        // Allow all thread policies temporarily in main thread to avoid
        // DiskWrite and UnBufferedIo violations during copying under
        // emulator environment.
        try (CloseableOnMainThread ignored =
                CloseableOnMainThread.StrictMode.allowAllThreadPolicies()) {
            ContextMenuUtils.selectContextMenuItem(
                    InstrumentationRegistry.getInstrumentation(),
                    sDownloadTestRule.getActivity(),
                    tab,
                    "testImage",
                    R.id.contextmenu_share_image);
        }

        ArgumentCaptor<ShareParams> shareParamsCaptor = ArgumentCaptor.forClass(ShareParams.class);
        ArgumentCaptor<ChromeShareExtras> chromeExtrasCaptor =
                ArgumentCaptor.forClass(ChromeShareExtras.class);
        verify(mShareDelegate)
                .share(
                        shareParamsCaptor.capture(),
                        chromeExtrasCaptor.capture(),
                        eq(ShareOrigin.CONTEXT_MENU));

        Assert.assertTrue(
                "Content being shared is expected to be image.",
                shareParamsCaptor.getValue().getFileContentType().startsWith("image"));
        Assert.assertTrue(
                "Share with share sheet expect to record the last used.",
                chromeExtrasCaptor.getValue().saveLastUsed());
    }

    @Test
    @SmallTest
    public void testShareLink() throws Exception {
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();

        // Set share delegate before triggering context menu, so the mocked share delegate is used.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    var supplier =
                            (ShareDelegateSupplier)
                                    ShareDelegateSupplier.from(
                                            sDownloadTestRule.getActivity().getWindowAndroid());
                    supplier.set(mShareDelegate);
                });
        ContextMenuUtils.selectContextMenuItem(
                InstrumentationRegistry.getInstrumentation(),
                sDownloadTestRule.getActivity(),
                tab,
                "testImage",
                R.id.contextmenu_share_link);

        verify(mShareDelegate).share(any(), any(), eq(ShareOrigin.CONTEXT_MENU));

        ArgumentCaptor<ShareParams> shareParamsCaptor = ArgumentCaptor.forClass(ShareParams.class);
        ArgumentCaptor<ChromeShareExtras> chromeExtrasCaptor =
                ArgumentCaptor.forClass(ChromeShareExtras.class);
        verify(mShareDelegate)
                .share(
                        shareParamsCaptor.capture(),
                        chromeExtrasCaptor.capture(),
                        eq(ShareOrigin.CONTEXT_MENU));

        Assert.assertFalse(
                "Link being shared is empty.",
                TextUtils.isEmpty(shareParamsCaptor.getValue().getUrl()));
        Assert.assertTrue(
                "Share with share sheet expect to record the last used.",
                chromeExtrasCaptor.getValue().saveLastUsed());
    }

    // TODO(benwgold): Add more test coverage for histogram recording of other context menu types.

    /**
     * Takes all the visible items on the menu and compares them to a the list of expected items.
     *
     * @param menu A context menu that is displaying visible items.
     * @param expectedItems A list of items that is expected to appear within a context menu. The
     *     list does not need to be ordered.
     */
    private void assertMenuItemsAreEqual(ContextMenuCoordinator menu, Integer... expectedItems) {
        List<Integer> actualItems = new ArrayList<>();
        for (int i = 0; i < menu.getCount(); i++) {
            if (menu.getItem(i).type >= CONTEXT_MENU_ITEM) {
                actualItems.add(menu.getItem(i).model.get(ContextMenuItemProperties.MENU_ID));
            }
        }

        assertThat(
                "Populated menu items were:" + getMenuTitles(menu),
                actualItems,
                Matchers.containsInAnyOrder(expectedItems));
    }

    private String getMenuTitles(ContextMenuCoordinator menu) {
        StringBuilder items = new StringBuilder();
        for (int i = 0; i < menu.getCount(); i++) {
            if (menu.getItem(i).type >= CONTEXT_MENU_ITEM) {
                items.append("\n")
                        .append(menu.getItem(i).model.get(ContextMenuItemProperties.TEXT));
            }
        }
        return items.toString();
    }

    private String getMenuTitleFromItem(ContextMenuCoordinator menu, int itemId) {
        StringBuilder itemName = new StringBuilder();
        for (int i = 0; i < menu.getCount(); i++) {
            if (menu.getItem(i).type >= CONTEXT_MENU_ITEM) {
                if (menu.getItem(i).model.get(ContextMenuItemProperties.MENU_ID) == itemId) {
                    itemName.append(menu.getItem(i).model.get(ContextMenuItemProperties.TEXT));
                    return itemName.toString();
                }
            }
        }
        return null;
    }

    /**
     * Adds items to the baseItems if the given condition is true.
     *
     * @param condition The condition to check for whether to add items or not.
     * @param baseItems The base list of items to add to.
     * @param additionalItems The additional items to add.
     * @return An array of items that has the additional items added if the condition is true.
     */
    private Integer[] addItemsIf(
            boolean condition, Integer[] baseItems, Integer[] additionalItems) {
        List<Integer> variableItems = new ArrayList<>();
        variableItems.addAll(Arrays.asList(baseItems));
        if (condition) {
            for (int i = 0; i < additionalItems.length; i++) variableItems.add(additionalItems[i]);
        }
        return variableItems.toArray(baseItems);
    }

    private void saveMediaFromContextMenu(
            String mediaDOMElement, int saveMenuID, String expectedFilename)
            throws TimeoutException, SecurityException, IOException {
        // Select "save [image/video]" in that menu.
        Tab tab = sDownloadTestRule.getActivity().getActivityTab();
        int callCount = sDownloadTestRule.getChromeDownloadCallCount();
        ContextMenuUtils.selectContextMenuItem(
                InstrumentationRegistry.getInstrumentation(),
                sDownloadTestRule.getActivity(),
                tab,
                mediaDOMElement,
                saveMenuID);

        // Wait for the download to complete and see if we got the right file
        Assert.assertTrue(sDownloadTestRule.waitForChromeDownloadToFinish(callCount));
        Assert.assertTrue(sDownloadTestRule.hasDownloaded(expectedFilename, null));
    }

    private String getClipboardText() throws Throwable {
        final AtomicReference<String> clipboardTextRef = new AtomicReference<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ClipboardManager clipMgr =
                            (ClipboardManager)
                                    sDownloadTestRule
                                            .getActivity()
                                            .getSystemService(Context.CLIPBOARD_SERVICE);
                    ClipData clipData = clipMgr.getPrimaryClip();
                    Assert.assertNotNull("Primary clip is null", clipData);
                    Assert.assertTrue(
                            "Primary clip contains no items.", clipData.getItemCount() > 0);
                    clipboardTextRef.set(clipData.getItemAt(0).getText().toString());
                });
        return clipboardTextRef.get();
    }

    /**
     * Hardcode image bytes to non-null arbitrary data.
     *
     * @param extension Image file extension.
     */
    private void hardcodeTestImageForSharing(String extension) {
        // This string just needs to be not empty in order for the code to accept it as valid
        // image data and generate the temp file for sharing. In the future we could explore
        // transcoding the actual test image from png to jpeg to make the test more realistic.
        String mockImageData = "randomdata";
        byte[] mockImageByteArray = mockImageData.getBytes();
        // See function javadoc for more context.
        ContextMenuNativeDelegateImpl.setHardcodedImageBytesForTesting(
                mockImageByteArray, extension);
    }

    private void assertStringContains(String subString, String superString) {
        Assert.assertTrue(
                "String '" + superString + "' does not contain '" + subString + "'",
                superString.contains(subString));
    }

    /**
     * Makes sure there are no files with names identical to the ones this test uses in the
     * downloads directory
     */
    private void deleteTestFiles() {
        sDownloadTestRule.deleteFilesInDownloadDirectory(TEST_FILES);
    }
}