chromium/content/public/test/android/javatests/src/org/chromium/content_public/browser/test/util/WebContentsUtils.java

// Copyright 2018 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.content_public.browser.test.util;

import androidx.annotation.Nullable;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.TerminationStatus;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.content.browser.input.SelectPopup;
import org.chromium.content.browser.selection.SelectionPopupControllerImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl;
import org.chromium.content_public.browser.GestureListenerManager;
import org.chromium.content_public.browser.ImeAdapter;
import org.chromium.content_public.browser.JavaScriptCallback;
import org.chromium.content_public.browser.RenderFrameHost;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.ViewEventSink;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;

import java.util.concurrent.TimeoutException;

/** Collection of test-only WebContents utilities. */
@JNINamespace("content")
public class WebContentsUtils {
    /**
     * Reports all frame submissions to the browser process, even those that do not impact Browser
     * UI.
     *
     * @param webContents The WebContents for which to report all frame submissions.
     * @param enabled Whether to report all frame submissions.
     */
    public static void reportAllFrameSubmissions(final WebContents webContents, boolean enabled) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    WebContentsUtilsJni.get().reportAllFrameSubmissions(webContents, enabled);
                });
    }

    /**
     * @param webContents The WebContents on which the SelectPopup is being shown.
     * @return {@code true} if select popup is being shown.
     */
    public static boolean isSelectPopupVisible(WebContents webContents) {
        return SelectPopup.fromWebContents(webContents).isVisibleForTesting();
    }

    /**
     * Gets the currently focused {@link RenderFrameHost} instance for a given {@link WebContents}.
     * @param webContents The WebContents in use.
     */
    public static RenderFrameHost getFocusedFrame(final WebContents webContents) {
        return WebContentsUtilsJni.get().getFocusedFrame(webContents);
    }

    /**
     * Issues a fake notification about the renderer being killed.
     *
     * @param webContents The WebContents in use.
     */
    public static void simulateRendererKilled(WebContents webContents) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> ((WebContentsImpl) webContents).simulateRendererKilledForTesting());
    }

    /**
     * Returns {@link ImeAdapter} instance associated with a given {@link WebContents}.
     *
     * @param webContents The WebContents in use.
     */
    public static ImeAdapter getImeAdapter(WebContents webContents) {
        return ThreadUtils.runOnUiThreadBlocking(() -> ImeAdapter.fromWebContents(webContents));
    }

    /**
     * Returns {@link GestureListenerManager} instance associated with a given {@link WebContents}.
     *
     * @param webContents The WebContents in use.
     */
    public static GestureListenerManager getGestureListenerManager(WebContents webContents) {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> GestureListenerManager.fromWebContents(webContents));
    }

    /**
     * Returns {@link ViewEventSink} instance associated with a given {@link WebContents}.
     *
     * @param webContents The WebContents in use.
     */
    public static ViewEventSink getViewEventSink(WebContents webContents) {
        return ThreadUtils.runOnUiThreadBlocking(() -> ViewEventSink.from(webContents));
    }

    /**
     * Injects the passed Javascript code in the current page and evaluates it, supplying a fake
     * user gesture. This also differs from {@link WebContents#evaluateJavaScript()} in that it
     * allows for executing script in non-webui frames.
     *
     * @param script The Javascript to execute.
     */
    public static void evaluateJavaScriptWithUserGesture(
            WebContents webContents, String script, @Nullable JavaScriptCallback callback) {
        if (script == null) return;
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        WebContentsUtilsJni.get()
                                .evaluateJavaScriptWithUserGesture(webContents, script, callback));
    }

    /**
     * Create and initialize a new {@link SelectionPopupController} instance for testing.
     *
     * @param webContents {@link WebContents} object.
     * @return {@link SelectionPopupController} object used for the give WebContents.
     *         Creates one if not present.
     */
    public static SelectionPopupController createSelectionPopupController(WebContents webContents) {
        return SelectionPopupControllerImpl.createForTesting(webContents);
    }

    /**
     * Checks if the given WebContents has a valid {@link ActionMode.Callback} set in place.
     * @return {@code true} if WebContents (its SelectionPopupController) has a valid
     *         action mode callback object.
     */
    public static boolean isActionModeSupported(WebContents webContents) {
        SelectionPopupControllerImpl controller =
                ((SelectionPopupControllerImpl)
                        SelectionPopupController.fromWebContents(webContents));
        return controller.isActionModeSupported();
    }

    /** Cause the renderer process for the given WebContents to crash. */
    public static void crashTabAndWait(WebContents webContents) throws TimeoutException {
        CallbackHelper callbackHelper = new CallbackHelper();
        WebContentsObserver observer =
                new WebContentsObserver() {
                    @Override
                    public void primaryMainFrameRenderProcessGone(
                            @TerminationStatus int terminationStatus) {
                        callbackHelper.notifyCalled();
                    }
                };
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    webContents.addObserver(observer);
                    WebContentsUtilsJni.get().crashTab(webContents);
                });
        callbackHelper.waitForOnly();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    webContents.removeObserver(observer);
                });
    }

    @CalledByNative
    private static void onEvaluateJavaScriptResult(String jsonResult, JavaScriptCallback callback) {
        callback.handleJavaScriptResult(jsonResult);
    }

    /**
     * Blocks the current execution until the primary main frame is in a steady state so the caller
     * can issue an `viz::CopyOutputRequest` against it.
     *
     * <p>See also, WaitForCopyableViewInFrame in content_browser_test_utils_internal.h.
     *
     * @param webContents The WebContents whose main frame we wish to wait on.
     */
    public static void waitForCopyableViewInWebContents(final WebContents webContents)
            throws TimeoutException {
        CallbackHelper callbackHelper = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    WebContentsUtilsJni.get()
                            .notifyCopyableViewInWebContents(
                                    webContents,
                                    () -> {
                                        callbackHelper.notifyCalled();
                                    });
                });
        callbackHelper.waitForOnly();
    }

    @NativeMethods
    interface Natives {
        void reportAllFrameSubmissions(WebContents webContents, boolean enabled);

        RenderFrameHost getFocusedFrame(WebContents webContents);

        void evaluateJavaScriptWithUserGesture(
                WebContents webContents, String script, @Nullable JavaScriptCallback callback);

        void crashTab(WebContents webContents);

        void notifyCopyableViewInWebContents(WebContents webContents, Runnable doneCallback);
    }
}