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

import android.os.SystemClock;

import org.junit.Assert;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.DOMUtils;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.concurrent.TimeoutException;

/** Extension of XrTestFramework meant for testing XR-related web APIs. */
public abstract class WebXrTestFramework extends XrTestFramework {
    private static final String TAG = "WebXrTestFramework";

    /**
     * Must be constructed after the rule has been applied (e.g. in whatever method is tagged
     * with @Before).
     */
    public WebXrTestFramework(ChromeActivityTestRule rule) {
        super(rule);
    }

    /**
     * WebVrTestFramework derives from this and overrides to allow WebVR tests to fail early if no
     * VRDisplay's were found. WebXR has no concept of a device, and inline support is always
     * available, so return true.
     *
     * @param webContents The WebContents to run the JavaScript through.
     * @return Whether an XRDevice was found.
     */
    public boolean xrDeviceFound(WebContents webContents) {
        return true;
    }

    /**
     * Helper function to run xrDeviceFound with the current tab's WebContents.
     *
     * @return Whether an XRDevice was found.
     */
    public boolean xrDeviceFound() {
        return xrDeviceFound(getCurrentWebContents());
    }

    /**
     * Enters a WebXR or WebVR session of some kind by tapping on the canvas on the page. Needs to
     * be non-static despite not using any member variables in order for the WebContents-less helper
     * version to work properly in subclasses.
     *
     * @param webContents The WebContents for the tab the canvas is in.
     */
    public void enterSessionWithUserGesture(WebContents webContents) {
        if (DEBUG_LOGS) Log.i(TAG, "enterSessionWithUserGesture");

        // This method includes multiple workarounds, see https://crbug.com/c/998307 for
        // context. In short, canvas clicks sometimes don't register after a transition
        // from a WebXR immersive session to VR Browser mode. Sometimes clickNode returns
        // false, but even when it returns true the click event doesn't always get
        // processed by JavaScript. Use a JavaScript variable to verify if the click was
        // received, and retry if it wasn't.
        boolean canvasClicked = false;
        runJavaScriptOrFail("canvasClicked=false", POLL_TIMEOUT_SHORT_MS, webContents);
        for (int i = 0; i < 3; ++i) {
            if (i > 0) {
                Log.e(TAG, "Failed to click canvas: retry #" + i);
            }
            boolean nodeClicked = false;
            try {
                nodeClicked =
                        DOMUtils.clickNode(
                                webContents, "webgl-canvas", /* goThroughRootAndroidView= */ false);
                if (DEBUG_LOGS) {
                    Log.i(TAG, "enterSessionWithUserGesture: nodeClicked => " + nodeClicked);
                }
                if (!nodeClicked) {
                    Log.e(TAG, "Failed to click canvas: clickNode is false");
                    // Since this path didn't involve a timeout, wait a bit before retrying.
                    SystemClock.sleep(1000);
                }
            } catch (TimeoutException e) {
                Log.e(TAG, "Failed to click canvas: " + e.toString());
            }
            if (nodeClicked) {
                canvasClicked = pollJavaScriptBoolean("canvasClicked", POLL_TIMEOUT_SHORT_MS);
                if (!canvasClicked) {
                    Log.e(TAG, "Failed to click canvas: canvasClicked is false");
                }
            } else {
                // nodeClicked is false, retry.
                continue;
            }

            if (canvasClicked) break;

            PropertyModel dialog =
                    ThreadUtils.runOnUiThreadBlocking(
                            () ->
                                    getRule()
                                            .getActivity()
                                            .getModalDialogManager()
                                            .getCurrentDialogForTest());

            // If we get here, "nodeClicked" is true but "canvasClicked" is false. Before
            // retrying, check if there's a dialog visible. Polling Javascript doesn't
            // work while a dialog is showing, and sometimes the click is handled quickly
            // enough for the consent prompt to show before we got a chance to check the
            // canvasClicked variable. In that case, assume we got the click and continue.
            if (dialog != null) {
                if (DEBUG_LOGS) Log.i(TAG, "Got a dialog, stop waiting for click");
                canvasClicked = true;
                break;
            }
        }

        if (!canvasClicked) {
            Assert.fail("Failed to click canvas to enter session: Click not received");
        }
    }

    /** Helper function to run enterSessionWithUserGesture using the current tab's WebContents. */
    public void enterSessionWithUserGesture() {
        enterSessionWithUserGesture(getCurrentWebContents());
    }

    /**
     * Enters a WebXR or WebVR session of some kind and waits until the page reports it is finished
     * with its JavaScript step. Needs to be non-static despite not using any member variables in
     * order for the WebContents-less helper version to work properly in subclasses.
     *
     * @param webContents The WebContents for the tab to enter the session in.
     */
    public void enterSessionWithUserGestureAndWait(WebContents webContents) {
        enterSessionWithUserGesture(webContents);
        waitOnJavaScriptStep(webContents);
    }

    /**
     * Helper function to run enterSessionWithUserGestureAndWait with the current tab's WebContents.
     */
    public void enterSessionWithUserGestureAndWait() {
        enterSessionWithUserGestureAndWait(getCurrentWebContents());
    }

    /**
     * Attempts to enter a WebXR or WebVR session of some kind, failing if it is unable to.
     *
     * @param webContents The WebContents for the tab to enter the session in.
     * @param needsCameraPermission True if the session requires Camera permission.
     */
    public abstract void enterSessionWithUserGestureOrFail(
            WebContents webContents, boolean needsCameraPermission);

    /**
     * Helper function to run enterSessionWithUserGestureOrFail with the current tab's WebContents.
     * Session will be treated as not requiring Camera permission.
     */
    public void enterSessionWithUserGestureOrFail() {
        enterSessionWithUserGestureOrFail(false);
    }

    /**
     * Helper function to run enterSessionWithUserGestureOrFail with the current tab's WebContents.
     *
     * @param needsCameraPermission True if the session requires Camera permission.
     */
    public void enterSessionWithUserGestureOrFail(boolean needsCameraPermission) {
        enterSessionWithUserGestureOrFail(getCurrentWebContents(), needsCameraPermission);
    }

    /**
     * Ends whatever type of session a subclass enters with enterSessionWithUserGesture.
     *
     * @param webContents The WebContents to end the session in.
     */
    public abstract void endSession(WebContents webContents);

    /** Helper function to run endSession with the current tab's WebContents. */
    public void endSession() {
        endSession(getCurrentWebContents());
    }

    /**
     * Helper function to run shouldExpectPermissionPrompt with the correct session type for the
     * framework.
     *
     * @param webContents The WebContents to check the permission prompt in.
     * @return True if the a request for the session type would trigger the permission prompt to be
     *     shown, otherwise false.
     */
    public abstract boolean shouldExpectPermissionPrompt(WebContents webContents);

    /**
     * Helper function to run shouldExpectPermissionPrompt with the current tab's WebContents.
     *
     * @return True if the a request for the session type would trigger the permission prompt to be
     *     shown, otherwise false.
     */
    public boolean shouldExpectPermissionPrompt() {
        return shouldExpectPermissionPrompt(getCurrentWebContents());
    }

    /**
     * Checks whether a session request of the given type is expected to trigger the consent dialog.
     *
     * @param sessionType The session type to pass to JavaScript defined in webxr_boilerplate.js,
     *     e.g. sessionTypes.AR
     * @param webContents The WebContents to check in.
     * @return True if the given session type is expected to trigger the permission prompt,
     *     otherwise false.
     */
    protected boolean shouldExpectPermissionPrompt(String sessionType, WebContents webContents) {
        return runJavaScriptOrFail(
                        "sessionTypeWouldTriggerConsent(" + sessionType + ")",
                        POLL_TIMEOUT_SHORT_MS,
                        webContents)
                .equals("true");
    }
}