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

// Copyright 2012 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 android.app.Activity;
import android.content.pm.ActivityInfo;
import android.view.View;
import android.view.ViewConfiguration;

import androidx.test.filters.SmallTest;

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.AwSettings;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.TestThreadUtils;

import java.util.Locale;

/** A test suite for zooming-related methods and settings. */
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
public class AwZoomTest extends AwParameterizedTest {
    @Rule public AwActivityTestRule mActivityTestRule;

    private TestAwContentsClient mContentsClient;
    private AwContents mAwContents;
    private static final float MAXIMUM_SCALE = 2.0f;
    private static final float EPSILON = 0.00001f;

    public AwZoomTest(AwSettingsMutation param) {
        this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
    }

    @Before
    public void setUp() {
        mContentsClient = new TestAwContentsClient();
        final AwTestContainerView testContainerView =
                mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient);
        mAwContents = testContainerView.getAwContents();
    }

    private String getZoomableHtml(float scale) {
        final int divWidthPercent = (int) (100.0f / scale);
        return String.format(
                Locale.US,
                "<html><head><meta name=\"viewport\" content=\"width=device-width,"
                        + " minimum-scale=%f, maximum-scale=%f, initial-scale=%f\"/></head><body"
                        + " style='margin:0'><div style='width:%d%%;height:100px;border:1px solid"
                        + " black'>Zoomable</div></body></html>",
                scale,
                MAXIMUM_SCALE,
                scale,
                divWidthPercent);
    }

    private String getNonZoomableHtml() {
        // This page can't be zoomed because its viewport fully occupies
        // view area and is explicitly made non user-scalable.
        return "<html><head>"
                + "<meta name=\"viewport\" "
                + "content=\"width=device-width,height=device-height,"
                + "initial-scale=1,maximum-scale=1,user-scalable=no\">"
                + "</head><body>Non-zoomable</body></html>";
    }

    private boolean isMultiTouchZoomSupportedOnUiThread() throws Throwable {
        return ThreadUtils.runOnUiThreadBlocking(() -> mAwContents.isMultiTouchZoomSupported());
    }

    private int getVisibilityOnUiThread(final View view) throws Throwable {
        return ThreadUtils.runOnUiThreadBlocking(() -> view.getVisibility());
    }

    private View getZoomControlsViewOnUiThread() throws Throwable {
        return ThreadUtils.runOnUiThreadBlocking(() -> mAwContents.getZoomControlsViewForTest());
    }

    private boolean canZoomInUsingZoomControls() throws Throwable {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.getZoomControlsForTest().canZoomIn());
    }

    private boolean canZoomOutUsingZoomControls() throws Throwable {
        return ThreadUtils.runOnUiThreadBlocking(
                () -> mAwContents.getZoomControlsForTest().canZoomOut());
    }

    private void invokeZoomPickerOnUiThread() {
        ThreadUtils.runOnUiThreadBlocking(() -> mAwContents.invokeZoomPicker());
        // Zoom picker is updated asynchronously.
        TestThreadUtils.flushNonDelayedLooperTasks();
    }

    private void zoomInOnUiThreadAndWait() throws Throwable {
        final float previousScale = mActivityTestRule.getPixelScaleOnUiThread(mAwContents);
        Assert.assertTrue(ThreadUtils.runOnUiThreadBlocking(() -> mAwContents.zoomIn()));
        // The zoom level is updated asynchronously.
        waitForScaleChange(previousScale);
    }

    private void zoomOutOnUiThreadAndWait() throws Throwable {
        final float previousScale = mActivityTestRule.getPixelScaleOnUiThread(mAwContents);
        Assert.assertTrue(ThreadUtils.runOnUiThreadBlocking(() -> mAwContents.zoomOut()));
        // The zoom level is updated asynchronously.
        waitForScaleChange(previousScale);
    }

    private void zoomByOnUiThreadAndWait(final float delta) throws Throwable {
        final float previousScale = mActivityTestRule.getPixelScaleOnUiThread(mAwContents);
        ThreadUtils.runOnUiThreadBlocking(() -> mAwContents.zoomBy(delta));
        // The zoom level is updated asynchronously.
        waitForScaleChange(previousScale);
    }

    private void waitForScaleChange(final float previousScale) {
        AwActivityTestRule.pollInstrumentationThread(
                () -> previousScale != mActivityTestRule.getPixelScaleOnUiThread(mAwContents));
    }

    private void waitForScaleToBecome(final float expectedScale) {
        AwActivityTestRule.pollInstrumentationThread(
                () ->
                        Math.abs(expectedScale - mActivityTestRule.getScaleOnUiThread(mAwContents))
                                < EPSILON);
    }

    private void waitUntilCanNotZoom() {
        AwActivityTestRule.pollInstrumentationThread(
                () ->
                        !mActivityTestRule.canZoomInOnUiThread(mAwContents)
                                && !mActivityTestRule.canZoomOutOnUiThread(mAwContents));
    }

    private void runMagnificationTest() throws Throwable {
        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setUseWideViewPort(true);
        Assert.assertFalse(
                "Should not be able to zoom in",
                mActivityTestRule.canZoomInOnUiThread(mAwContents));
        final float pageMinimumScale = 0.5f;
        mActivityTestRule.loadDataSync(
                mAwContents,
                mContentsClient.getOnPageFinishedHelper(),
                getZoomableHtml(pageMinimumScale),
                "text/html",
                false);
        waitForScaleToBecome(pageMinimumScale);
        Assert.assertTrue(
                "Should be able to zoom in", mActivityTestRule.canZoomInOnUiThread(mAwContents));
        Assert.assertFalse(
                "Should not be able to zoom out",
                mActivityTestRule.canZoomOutOnUiThread(mAwContents));

        while (mActivityTestRule.canZoomInOnUiThread(mAwContents)) {
            zoomInOnUiThreadAndWait();
        }
        Assert.assertTrue(
                "Should be able to zoom out", mActivityTestRule.canZoomOutOnUiThread(mAwContents));

        while (mActivityTestRule.canZoomOutOnUiThread(mAwContents)) {
            zoomOutOnUiThreadAndWait();
        }
        Assert.assertTrue(
                "Should be able to zoom in", mActivityTestRule.canZoomInOnUiThread(mAwContents));

        zoomByOnUiThreadAndWait(4.0f);
        waitForScaleToBecome(MAXIMUM_SCALE);

        zoomByOnUiThreadAndWait(0.5f);
        waitForScaleToBecome(MAXIMUM_SCALE * 0.5f);

        zoomByOnUiThreadAndWait(0.01f);
        waitForScaleToBecome(pageMinimumScale);
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testMagnification() throws Throwable {
        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setSupportZoom(true);
        runMagnificationTest();
    }

    // According to Android CTS test, zoomIn/Out must work
    // even if supportZoom is turned off.
    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testMagnificationWithZoomSupportOff() throws Throwable {
        mActivityTestRule.getAwSettingsOnUiThread(mAwContents).setSupportZoom(false);
        runMagnificationTest();
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    @SkipMutations(reason = "This test depends on AwSettings.setSupportZoom(true)")
    public void testZoomUsingMultiTouch() throws Throwable {
        AwSettings webSettings = mActivityTestRule.getAwSettingsOnUiThread(mAwContents);
        mActivityTestRule.loadDataSync(
                mAwContents,
                mContentsClient.getOnPageFinishedHelper(),
                getZoomableHtml(0.5f),
                "text/html",
                false);

        Assert.assertTrue(webSettings.supportZoom());
        Assert.assertFalse(webSettings.getBuiltInZoomControls());
        Assert.assertFalse(isMultiTouchZoomSupportedOnUiThread());

        webSettings.setBuiltInZoomControls(true);
        Assert.assertTrue(isMultiTouchZoomSupportedOnUiThread());

        webSettings.setSupportZoom(false);
        Assert.assertFalse(isMultiTouchZoomSupportedOnUiThread());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    @SkipMutations(reason = "This test depends on AwSettings.setSupportZoom(true)")
    public void testZoomControls() throws Throwable {
        AwSettings webSettings = mActivityTestRule.getAwSettingsOnUiThread(mAwContents);
        webSettings.setUseWideViewPort(true);
        Assert.assertFalse(
                "Should not be able to zoom in",
                mActivityTestRule.canZoomInOnUiThread(mAwContents));
        final float pageMinimumScale = 0.5f;
        mActivityTestRule.loadDataSync(
                mAwContents,
                mContentsClient.getOnPageFinishedHelper(),
                getZoomableHtml(pageMinimumScale),
                "text/html",
                false);
        waitForScaleToBecome(pageMinimumScale);
        // It must be possible to zoom in (or zoom out) for zoom controls to be shown
        Assert.assertTrue(
                "Should be able to zoom in", mActivityTestRule.canZoomInOnUiThread(mAwContents));

        Assert.assertTrue(webSettings.supportZoom());
        webSettings.setBuiltInZoomControls(true);
        webSettings.setDisplayZoomControls(false);

        // With DisplayZoomControls set to false, attempts to display zoom
        // controls must be ignored.
        Assert.assertNull(getZoomControlsViewOnUiThread());
        invokeZoomPickerOnUiThread();
        Assert.assertNull(getZoomControlsViewOnUiThread());

        webSettings.setDisplayZoomControls(true);
        Assert.assertNull(getZoomControlsViewOnUiThread());
        invokeZoomPickerOnUiThread();
        View zoomControls = getZoomControlsViewOnUiThread();
        Assert.assertEquals(View.VISIBLE, getVisibilityOnUiThread(zoomControls));
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    @SkipMutations(reason = "This test depends on AwSettings.setSupportZoom(true)")
    public void testZoomControlsUiIsUpdatedOnChanges() throws Throwable {
        AwSettings webSettings = mActivityTestRule.getAwSettingsOnUiThread(mAwContents);
        webSettings.setDisplayZoomControls(true);
        webSettings.setBuiltInZoomControls(true);
        webSettings.setUseWideViewPort(true);
        Assert.assertFalse(canZoomInUsingZoomControls());
        Assert.assertFalse(canZoomOutUsingZoomControls());
        final float pageMinimumScale = 0.5f;
        mActivityTestRule.loadDataSync(
                mAwContents,
                mContentsClient.getOnPageFinishedHelper(),
                getZoomableHtml(pageMinimumScale),
                "text/html",
                false);
        waitForScaleToBecome(pageMinimumScale);
        Assert.assertTrue(canZoomInUsingZoomControls());
        Assert.assertFalse(canZoomOutUsingZoomControls());

        zoomInOnUiThreadAndWait();
        Assert.assertTrue(canZoomInUsingZoomControls());
        Assert.assertTrue(canZoomOutUsingZoomControls());

        zoomOutOnUiThreadAndWait();
        Assert.assertTrue(canZoomInUsingZoomControls());
        Assert.assertFalse(canZoomOutUsingZoomControls());
    }

    @Test
    @SmallTest
    @Feature({"AndroidWebView"})
    @SkipMutations(reason = "This test depends on AwSettings.setSupportZoom(true)")
    public void testZoomControlsOnNonZoomableContent() throws Throwable {
        AwSettings webSettings = mActivityTestRule.getAwSettingsOnUiThread(mAwContents);
        mActivityTestRule.loadDataSync(
                mAwContents,
                mContentsClient.getOnPageFinishedHelper(),
                getNonZoomableHtml(),
                "text/html",
                false);

        // ContentView must update itself according to the viewport setup.
        waitUntilCanNotZoom();

        Assert.assertTrue(webSettings.supportZoom());
        webSettings.setBuiltInZoomControls(true);
        webSettings.setDisplayZoomControls(true);
        Assert.assertNull(getZoomControlsViewOnUiThread());
        invokeZoomPickerOnUiThread();
        View zoomControls = getZoomControlsViewOnUiThread();
        Assert.assertEquals(View.GONE, getVisibilityOnUiThread(zoomControls));
    }

    @Test
    @DisableHardwareAcceleration
    @SmallTest
    @Feature({"AndroidWebView"})
    @SkipMutations(reason = "This test depends on AwSettings.setSupportZoom(true)")
    public void testZoomControlsOnOrientationChange() throws Throwable {
        AwSettings webSettings = mActivityTestRule.getAwSettingsOnUiThread(mAwContents);
        mActivityTestRule.loadDataSync(
                mAwContents,
                mContentsClient.getOnPageFinishedHelper(),
                getZoomableHtml(0.5f),
                "text/html",
                false);

        Assert.assertTrue(webSettings.supportZoom());
        webSettings.setBuiltInZoomControls(true);
        webSettings.setDisplayZoomControls(true);
        invokeZoomPickerOnUiThread();

        // Now force an orientation change, and try to display the zoom picker
        // again. Make sure that we don't crash when the ZoomPicker registers
        // its receiver.

        Activity activity = mActivityTestRule.getActivity();
        int orientation = activity.getRequestedOrientation();
        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        activity.setRequestedOrientation(orientation);
        invokeZoomPickerOnUiThread();

        // We may crash shortly (as the zoom picker has a short delay in it before
        // it tries to register its BroadcastReceiver), so sleep to verify we don't.
        // The delay is encoded in ZoomButtonsController#ZOOM_CONTROLS_TIMEOUT,
        // if that changes we may need to update this test.
        Thread.sleep(ViewConfiguration.getZoomControlsTimeout());
    }
}