chromium/android_webview/javatests/src/org/chromium/android_webview/test/ContextMenuTest.java

// Copyright 2024 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.android_webview.test;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.equalTo;

import static org.chromium.android_webview.test.devui.DeveloperUiTestUtils.getClipBoardTextOnUiThread;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.util.Pair;
import android.view.KeyEvent;

import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.intent.matcher.IntentMatchers;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;

import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.common.AwFeatures;
import org.chromium.android_webview.contextmenu.AwContextMenuHeaderCoordinator;
import org.chromium.android_webview.contextmenu.AwContextMenuItem;
import org.chromium.android_webview.contextmenu.AwContextMenuItem.Item;
import org.chromium.android_webview.contextmenu.AwContextMenuItemDelegate;
import org.chromium.android_webview.contextmenu.AwContextMenuPopulator;
import org.chromium.android_webview.test.AwActivityTestRule.TestDependencyFactory;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features;
import org.chromium.components.embedder_support.contextmenu.ContextMenuParams;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.net.test.util.TestWebServer;
import org.chromium.ui.base.MenuSourceType;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.url.GURL;

import java.util.List;

/** Tests for context menu methods */
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
@Batch(Batch.PER_CLASS)
@Features.EnableFeatures({AwFeatures.WEBVIEW_HYPERLINK_CONTEXT_MENU})
public class ContextMenuTest extends AwParameterizedTest {
    private static final String FILE = "/main.html";
    private static final String DATA =
            "<html><head></head><body>"
                    + "<a href='test_link.html' id='testLink'>Test Link</a>"
                    + "</body></html>";

    @Rule public AwActivityTestRule mRule;

    private TestWebServer mWebServer;
    private AwTestContainerView mTestContainerView;
    private TestAwContentsClient mContentsClient;
    private CallbackHelper mCallbackHelper = new CallbackHelper();
    private AwContents mAwContents;
    private Context mContext;

    public ContextMenuTest(AwSettingsMutation param) {
        mRule = new AwActivityTestRule(param.getMutation());
    }

    @Before
    public void setUp() throws Exception {
        mWebServer = TestWebServer.start();
        mContentsClient = new TestAwContentsClient();
        mTestContainerView =
                mRule.createAwTestContainerViewOnMainSync(
                        mContentsClient, false, new TestDependencyFactory());
        mAwContents = mTestContainerView.getAwContents();
        mContext = mAwContents.getWebContents().getTopLevelNativeWindow().getContext().get();
        AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
    }

    @After
    public void tearDown() {
        if (mWebServer != null) {
            mWebServer.shutdown();
        }
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    @SkipMutations(
        reason = "This test uses DOMUtils.longPressNode() which is known"
        + " to be flaky under modified scaling factor, see crbug.com/40840940")
    public void testCopyLinkText() throws Throwable {
        int item = Item.COPY_LINK_TEXT;

        final String url = mWebServer.setResponse(FILE, DATA, null);
        loadUrlSync(url);
        DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "testLink");

        DOMUtils.longPressNode(mAwContents.getWebContents(), "testLink");

        onView(withText(getTitle(mContext, item))).perform(click());

        Assert.assertEquals("Test Link", getClipBoardTextOnUiThread(mContext));
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    @SkipMutations(
        reason = "This test uses DOMUtils.longPressNode() which is known"
        + " to be flaky under modified scaling factor, see crbug.com/40840940")
    public void testCopyLinkURL() throws Throwable {
        int item = Item.COPY_LINK_ADDRESS;

        final String url = mWebServer.setResponse(FILE, DATA, null);
        loadUrlSync(url);
        DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "testLink");

        DOMUtils.longPressNode(mAwContents.getWebContents(), "testLink");

        onView(withText(getTitle(mContext, item))).perform(click());

        assertStringContains("test_link.html", getClipBoardTextOnUiThread(mContext));
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    @SkipMutations(
        reason = "This test uses DOMUtils.longPressNode() which is known"
        + " to be flaky under modified scaling factor, see crbug.com/40840940")
    public void testOpenInBrowser() throws Throwable {
        try {
            Intents.init();
            // Before triggering the viewing intent, stub it out to avoid cascading that into
            // further intents and opening the web browser.
            intending(IntentMatchers.hasAction(equalTo(Intent.ACTION_VIEW)))
                    .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

            int item = Item.OPEN_IN_BROWSER;

            final String url = mWebServer.setResponse(FILE, DATA, null);
            loadUrlSync(url);
            DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "testLink");

            DOMUtils.longPressNode(mAwContents.getWebContents(), "testLink");

            onView(withText(getTitle(mContext, item))).perform(click());

            intended(IntentMatchers.hasAction(equalTo(Intent.ACTION_VIEW)));
        } finally {
            Intents.release();
        }
    }

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    @SkipMutations(
        reason = "This test uses DOMUtils.longPressNode() which is known"
        + " to be flaky under modified scaling factor, see crbug.com/40840940")
    public void testDismissContextMenuOnBack() throws Throwable {
        final String url = mWebServer.setResponse(FILE, DATA, null);
        loadUrlSync(url);
        DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "testLink");

        DOMUtils.longPressNode(mAwContents.getWebContents(), "testLink");

        CriteriaHelper.pollUiThread(
                () -> {
                    return !mRule.getActivity().hasWindowFocus();
                },
                "Context menu did not have window focus");

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

    @Test
    @MediumTest
    @Feature({"AndroidWebView"})
    @SkipMutations(
        reason = "This test uses DOMUtils.longPressNode() which is known"
        + " to be flaky under modified scaling factor, see crbug.com/40840940")
    public void testDismissContextMenuOnClick() throws Throwable {
        final String url = mWebServer.setResponse(FILE, DATA, null);
        loadUrlSync(url);
        DOMUtils.waitForNonZeroNodeBounds(mAwContents.getWebContents(), "testLink");

        DOMUtils.longPressNode(mAwContents.getWebContents(), "testLink");

        CriteriaHelper.pollUiThread(
                () -> {
                    return !mRule.getActivity().hasWindowFocus();
                },
                "Context menu did not have window focus");

        onView(withText(getTitle(mContext, Item.COPY_LINK_ADDRESS))).perform(click());

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

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testBuildingContextMenuItems() throws Throwable {
        Integer[] expectedItems = {
            R.id.contextmenu_copy_link_text,
            R.id.contextmenu_copy_link_address,
            R.id.contextmenu_open_in_browser_id,
        };

        ContextMenuParams params =
                new ContextMenuParams(
                        0,
                        0,
                        new GURL("http://www.example.com/page_url"),
                        new GURL("http://www.example.com/other_example"),
                        "BLAH!",
                        GURL.emptyGURL(),
                        GURL.emptyGURL(),
                        "",
                        null,
                        false,
                        0,
                        0,
                        MenuSourceType.MENU_SOURCE_TOUCH,
                        false,
                        /* additionalNavigationParams= */ null);

        AwContextMenuItemDelegate itemDelegate =
                new AwContextMenuItemDelegate(
                        mRule.getActivity(), mAwContents.getWebContents(), params);

        AwContextMenuPopulator populator =
                new AwContextMenuPopulator(mContext, itemDelegate, params);

        List<Pair<Integer, ModelList>> contextMenuState = populator.buildContextMenu();

        ModelList items = contextMenuState.get(0).second;

        Integer[] actualItems = new Integer[items.size()];

        for (int i = 0; i < items.size(); i++) {
            actualItems[i] = populator.getMenuId(items.get(i).model);
        }

        Assert.assertArrayEquals(actualItems, expectedItems);
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testHeaderHasURLText() throws Throwable {
        String expectedHeaderText = "http://www.testurl.com/first_page";

        ContextMenuParams params =
                new ContextMenuParams(
                        0,
                        0,
                        new GURL("http://www.example.com/page_url"),
                        GURL.emptyGURL(),
                        "BLAH!",
                        new GURL(expectedHeaderText),
                        GURL.emptyGURL(),
                        "",
                        null,
                        false,
                        0,
                        0,
                        MenuSourceType.MENU_SOURCE_TOUCH,
                        false,
                        /* additionalNavigationParams= */ null);

        AwContextMenuHeaderCoordinator headerCoordinator =
                new AwContextMenuHeaderCoordinator(params);

        String actualHeaderTitle = headerCoordinator.getTitle();

        Assert.assertEquals(actualHeaderTitle, expectedHeaderText);
    }

    private void loadUrlSync(String url) throws Exception {
        CallbackHelper done = mContentsClient.getOnPageCommitVisibleHelper();
        int callCount = done.getCallCount();
        mRule.loadUrlSync(
                mTestContainerView.getAwContents(), mContentsClient.getOnPageFinishedHelper(), url);
        done.waitForCallback(callCount);
    }

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

    private String getTitle(Context context, @Item int item) {
        return AwContextMenuItem.getTitle(context, item).toString();
    }
}