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

import org.hamcrest.Matchers;
import org.junit.Assert;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.CriteriaNotSatisfiedException;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.fullscreen.FullscreenManagerTestUtils;
import org.chromium.chrome.browser.tab.TabStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.Coordinates;
import org.chromium.content_public.browser.test.util.JavaScriptUtils;
import org.chromium.content_public.browser.test.util.TouchCommon;

import java.util.concurrent.TimeUnit;

/** Utility class providing viewport helpers for tests. */
public final class ViewportTestUtils {

    private boolean mSetupCalled;

    private final ChromeTabbedActivityTestRule mActivityTestRule;

    private static final int TEST_TIMEOUT = 10000;

    public ViewportTestUtils(ChromeTabbedActivityTestRule rule) {
        mActivityTestRule = rule;
    }

    /**
     * Sets up the test for browser controls.
     *
     * <p>Removes the three-second delay after a page load where browser controls are immovable so
     * the tests can move the browser controls.
     */
    public void setUpForBrowserControls() {
        ThreadUtils.runOnUiThreadBlocking(
                TabStateBrowserControlsVisibilityDelegate::disablePageLoadDelayForTests);
        FullscreenManagerTestUtils.disableBrowserOverrides();
        mSetupCalled = true;
    }

    public void waitForBrowserControlsState(boolean shown) {
        int topControlsHeight = getTopControlsHeightPx();
        BrowserControlsStateProvider browserControlsStateProvider =
                mActivityTestRule.getActivity().getBrowserControlsManager();
        // The TopControlOffset is the offset of the controls top edge from the viewport top edge.
        // So fully shown the offset is 0, fully hidden it is -controls_height.
        int expectedOffset = shown ? 0 : -topControlsHeight;

        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            browserControlsStateProvider.getTopControlOffset(),
                            Matchers.is(expectedOffset));
                },
                TEST_TIMEOUT,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    public void hideBrowserControls() throws Throwable {
        Assert.assertTrue(mSetupCalled);

        // Ensure controls start fully shown. A new renderer initializes with controls hidden and
        // receives a signal to animate them to showing. Trying to hide the controls before that
        // animation has completed is flaky.
        waitForBrowserControlsState(/* shown= */ true);

        FullscreenManagerTestUtils.waitForPageToBeScrollable(
                mActivityTestRule.getActivity().getActivityTab());
        waitForFramePresented();

        int initialPageHeight = getPageInnerHeightPx();
        int topControlsHeight = getTopControlsHeightPx();

        float dragX = 50f;

        // Drag slightly less than the full height of the controls. Releasing at this point will
        // animate the controls to hidden but ensure we don't accidentally cause any scrolling of
        // the page.
        float dragStartY = topControlsHeight * 3;
        float dragEndY = dragStartY - topControlsHeight * 0.85f;

        long duration_ms = 1000;
        int steps = 60;
        TouchCommon.performDragNoFling(
                mActivityTestRule.getActivity(),
                dragX,
                dragX,
                dragStartY,
                dragEndY,
                steps,
                duration_ms);

        waitForBrowserControlsState(/* shown= */ false);
        // Also wait for the browser controls to resize Blink before returning.
        waitForExpectedPageHeight(initialPageHeight + getTopControlsHeightDp());
    }

    public void waitForExpectedPageHeight(double expectedPageHeight) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    try {
                        int curHeight = getPageInnerHeightPx();
                        // Allow 1px delta to account for device scale factor rounding.
                        Criteria.checkThat(
                                (double) curHeight,
                                Matchers.closeTo(expectedPageHeight, /* error= */ 1.0));
                    } catch (Throwable e) {
                        throw new CriteriaNotSatisfiedException(e);
                    }
                },
                TEST_TIMEOUT,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    public void waitForExpectedVisualViewportHeight(double expectedHeight) {
        CriteriaHelper.pollInstrumentationThread(
                () -> {
                    try {
                        double curHeight = getVisualViewportHeightPx();
                        // Allow 1px delta to account for device scale factor rounding.
                        Criteria.checkThat(
                                curHeight, Matchers.closeTo(expectedHeight, /* error= */ 1.0));
                    } catch (Throwable e) {
                        throw new CriteriaNotSatisfiedException(e);
                    }
                },
                TEST_TIMEOUT,
                CriteriaHelper.DEFAULT_POLLING_INTERVAL);
    }

    // Force generating a new compositor frame from the renderer and wait until
    // its presented on screen.
    public void waitForFramePresented() throws Throwable {
        final CallbackHelper ch = new CallbackHelper();

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    getWebContents()
                            .getMainFrame()
                            .insertVisualStateCallback(result -> ch.notifyCalled());
                });

        ch.waitForNext(TEST_TIMEOUT, TimeUnit.SECONDS);

        // insertVisualStateCallback replies when a CompositorFrame is submitted. However, we want
        // to wait until the Viz process has received the new CompositorFrame so that the new frame
        // is available to a CopySurfaceRequest. Waiting for a second frame to be submitted
        // guarantees this since it cannot be sent until the first frame was ACKed by Viz.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    getWebContents()
                            .getMainFrame()
                            .insertVisualStateCallback(result -> ch.notifyCalled());
                });

        ch.waitForNext(TEST_TIMEOUT, TimeUnit.SECONDS);
    }

    public double getDeviceScaleFactor() {
        return Coordinates.createFor(getWebContents()).getDeviceScaleFactor();
    }

    public int getTopControlsHeightPx() {
        BrowserControlsStateProvider browserControlsStateProvider =
                mActivityTestRule.getActivity().getBrowserControlsManager();
        return browserControlsStateProvider.getTopControlsHeight();
    }

    public int getTopControlsHeightDp() {
        return (int) Math.floor(getTopControlsHeightPx() / getDeviceScaleFactor());
    }

    public int getPageInnerHeightPx() throws Throwable {
        return Integer.parseInt(
                JavaScriptUtils.executeJavaScriptAndWaitForResult(
                        getWebContents(), "window.innerHeight"));
    }

    public double getVisualViewportHeightPx() throws Throwable {
        return Float.parseFloat(
                JavaScriptUtils.executeJavaScriptAndWaitForResult(
                        getWebContents(), "window.visualViewport.height"));
    }

    private WebContents getWebContents() {
        return mActivityTestRule.getActivity().getActivityTab().getWebContents();
    }
}