blob: 1eed020c0c20b882c3d4a68291a95eceb541cf8c [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.webkit.cts;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import android.net.http.SslError;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
import android.platform.test.annotations.AppModeFull;
import android.test.ActivityInstrumentationTestCase2;
import android.webkit.SslErrorHandler;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
import com.android.compatibility.common.util.NullWebViewUtils;
import com.android.compatibility.common.util.PollingCheck;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Test WebView zooming behaviour
*/
@AppModeFull
public class WebViewZoomTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
private WebView mWebView;
private WebViewOnUiThread mOnUiThread;
private ScaleChangedWebViewClient mWebViewClient;
private CtsTestServer mWebServer;
/**
* Epsilon used in page scale value comparisons.
*/
private static final float PAGE_SCALE_EPSILON = 0.0001f;
public WebViewZoomTest() {
super("com.android.cts.webkit", WebViewCtsActivity.class);
}
@Override
protected void setUp() throws Exception {
super.setUp();
final WebViewCtsActivity activity = getActivity();
mWebView = activity.getWebView();
if (mWebView == null)
return;
mOnUiThread = new WebViewOnUiThread(mWebView);
mOnUiThread.requestFocus();
new PollingCheck() {
@Override
protected boolean check() {
return activity.hasWindowFocus();
}
}.run();
mWebViewClient = new ScaleChangedWebViewClient();
mOnUiThread.setWebViewClient(mWebViewClient);
// Pinch zoom is not supported in wrap_content layouts.
mOnUiThread.setLayoutHeightToMatchParent();
}
@Override
protected void tearDown() throws Exception {
if (mOnUiThread != null) {
mOnUiThread.cleanUp();
}
if (mWebServer != null) {
stopWebServer();
}
super.tearDown();
}
private void stopWebServer() throws Exception {
assertNotNull(mWebServer);
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
ThreadPolicy tmpPolicy = new ThreadPolicy.Builder(oldPolicy)
.permitNetwork()
.build();
StrictMode.setThreadPolicy(tmpPolicy);
mWebServer.shutdown();
mWebServer = null;
StrictMode.setThreadPolicy(oldPolicy);
}
private void setUpPage() throws Exception {
assertFalse(mWebViewClient.onScaleChangedCalled());
assertNull(mWebServer);
// Pass CtsTestserver.SslMode.TRUST_ANY_CLIENT to make the server serve https URLs yet do
// not ask client for client authentication.
mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.TRUST_ANY_CLIENT);
mOnUiThread.loadUrlAndWaitForCompletion(
mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
pollingCheckForCanZoomIn();
}
public void testZoomIn() throws Throwable {
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
setUpPage();
assertTrue(mOnUiThread.zoomIn());
mWebViewClient.waitForNextScaleChange();
}
@SuppressWarnings("deprecation")
public void testGetZoomControls() {
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
WebSettings settings = mOnUiThread.getSettings();
assertTrue(settings.supportZoom());
assertNotNull(
"Should be able to get zoom controls when zoom is enabled",
WebkitUtils.onMainThreadSync(() -> { return mWebView.getZoomControls(); }));
// disable zoom support
settings.setSupportZoom(false);
assertFalse(settings.supportZoom());
assertNull(
"Should not be able to get zoom controls when zoom is disabled",
WebkitUtils.onMainThreadSync(() -> { return mWebView.getZoomControls(); }));
}
public void testInvokeZoomPicker() throws Exception {
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
WebSettings settings = mOnUiThread.getSettings();
assertTrue(settings.supportZoom());
setUpPage();
WebkitUtils.onMainThreadSync(() -> mWebView.invokeZoomPicker());
}
public void testZoom_canNotZoomInPastMaximum() {
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
float currScale = mOnUiThread.getScale();
// Zoom in until maximum scale, in default increments.
while (mOnUiThread.zoomIn()) {
currScale = mWebViewClient.expectZoomIn(currScale);
}
assertFalse(mOnUiThread.zoomIn());
assertNoScaleChange(currScale);
}
public void testZoom_canNotZoomOutPastMinimum() {
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
float currScale = mOnUiThread.getScale();
// Zoom in until maximum scale, in default increments.
while (mOnUiThread.zoomOut()) {
currScale = mWebViewClient.expectZoomOut(currScale);
}
assertFalse(mOnUiThread.zoomOut());
assertNoScaleChange(currScale);
}
public void testCanZoomWhileZoomSupportedFalse() throws Throwable {
// setZoomSupported disables user controls, but not zooming via API
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
setUpPage();
WebSettings settings = mOnUiThread.getSettings();
settings.setSupportZoom(false);
assertFalse(settings.supportZoom());
float currScale = mOnUiThread.getScale();
assertTrue("Zoom out should succeed although zoom support is disabled in web settings",
mOnUiThread.zoomIn());
currScale = mWebViewClient.expectZoomIn(currScale);
assertTrue("Zoom out should succeed although zoom support is disabled in web settings",
mOnUiThread.zoomOut());
currScale = mWebViewClient.expectZoomOut(currScale);
}
public void testZoomByPowerOfTwoIncrements() throws Throwable {
// setZoomSupported disables user controls, but not zooming via API
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
setUpPage();
float currScale = mOnUiThread.getScale();
mOnUiThread.zoomBy(1.25f); // zoom in
currScale = mWebViewClient.expectZoomBy(currScale, 1.25f);
mOnUiThread.zoomBy(0.75f); // zoom out
currScale = mWebViewClient.expectZoomBy(currScale, 0.75f);
}
public void testZoomByNonPowerOfTwoIncrements() throws Throwable {
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
setUpPage();
float currScale = mOnUiThread.getScale();
// Zoom in until maximum scale, in specified increments designed so that the last zoom will
// be less than expected.
while (mOnUiThread.canZoomIn()) {
mOnUiThread.zoomBy(1.7f);
currScale = mWebViewClient.expectZoomBy(currScale, 1.7f);
}
// At this point, zooming in should do nothing.
mOnUiThread.zoomBy(1.7f);
assertNoScaleChange(currScale);
// Zoom out until minimum scale, in specified increments designed so that the last zoom will
// be less than requested.
while (mOnUiThread.canZoomOut()) {
mOnUiThread.zoomBy(0.7f);
currScale = mWebViewClient.expectZoomBy(currScale, 0.7f);
}
// At this point, zooming out should do nothing.
mOnUiThread.zoomBy(0.7f);
assertNoScaleChange(currScale);
}
public void testScaleChangeCallbackMatchesGetScale() throws Throwable {
if (!NullWebViewUtils.isWebViewAvailable()) {
return;
}
assertFalse(mWebViewClient.onScaleChangedCalled());
setUpPage();
assertFalse(mWebViewClient.onScaleChangedCalled());
assertTrue(mOnUiThread.zoomIn());
ScaleChangedState state = mWebViewClient.waitForNextScaleChange();
assertEquals(
"Expected onScaleChanged arg 2 (new scale) to equal view.getScale()",
state.mNewScale, mOnUiThread.getScale());
}
private void assertNoScaleChange(float currScale) {
// We sleep to assert to the best of our ability
// that a scale change does *not* happen.
try {
Thread.sleep(500);
assertFalse(mWebViewClient.onScaleChangedCalled());
assertEquals(currScale, mOnUiThread.getScale());
} catch (InterruptedException e) {
fail("Interrupted");
}
}
private static final class ScaleChangedState {
public float mOldScale;
public float mNewScale;
public boolean mCanZoomIn;
public boolean mCanZoomOut;
ScaleChangedState(WebView view, float oldScale, float newScale) {
mOldScale = oldScale;
mNewScale = newScale;
mCanZoomIn = view.canZoomIn();
mCanZoomOut = view.canZoomOut();
}
}
private void pollingCheckForCanZoomIn() {
new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
@Override
protected boolean check() {
return mOnUiThread.canZoomIn();
}
}.run();
}
private final class ScaleChangedWebViewClient extends WaitForLoadedClient {
private BlockingQueue<ScaleChangedState> mCallQueue;
public ScaleChangedWebViewClient() {
super(mOnUiThread);
mCallQueue = new LinkedBlockingQueue<>();
}
@Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
super.onScaleChanged(view, oldScale, newScale);
mCallQueue.add(new ScaleChangedState(view, oldScale, newScale));
}
public float expectZoomBy(float currentScale, float scaleAmount) {
assertTrue(scaleAmount != 1.0f);
float nextScale = currentScale * scaleAmount;
ScaleChangedState state = waitForNextScaleChange();
assertEquals(currentScale, state.mOldScale);
// Zoom scale changes can come in multiple steps and the initial scale may have
// conversion errors. Wait for the first significant scale change.
while (Math.abs(state.mNewScale - state.mOldScale) < PAGE_SCALE_EPSILON) {
state = waitForNextScaleChange();
}
// Check that we zoomed in the expected direction wrt. the current scale.
if (scaleAmount > 1.0f) {
assertThat(
"Expected new scale > current scale when zooming in",
state.mNewScale, greaterThan(currentScale));
} else {
assertThat(
"Expected new scale < current scale when zooming out",
state.mNewScale, lessThan(currentScale));
}
// If we hit the zoom limit, then the new scale should be between the old scale
// and the expected new scale. Otherwise it should equal the expected new scale.
if (Math.abs(nextScale - state.mNewScale) > PAGE_SCALE_EPSILON) {
if (scaleAmount > 1.0f) {
assertFalse(state.mCanZoomIn);
assertThat(
"Expected new scale <= requested scale when zooming in past limit",
state.mNewScale, lessThanOrEqualTo(nextScale));
} else {
assertFalse(state.mCanZoomOut);
assertThat(
"Expected new scale >= requested scale when zooming out past limit",
state.mNewScale, greaterThanOrEqualTo(nextScale));
}
}
return state.mNewScale;
}
public float expectZoomOut(float currentScale) {
ScaleChangedState state = waitForNextScaleChange();
assertEquals(currentScale, state.mOldScale);
assertThat(state.mNewScale, lessThan(currentScale));
return state.mNewScale;
}
public float expectZoomIn(float currentScale) {
ScaleChangedState state = waitForNextScaleChange();
assertEquals(currentScale, state.mOldScale);
assertThat(state.mNewScale, greaterThan(currentScale));
return state.mNewScale;
}
public ScaleChangedState waitForNextScaleChange() {
return WebkitUtils.waitForNextQueueElement(mCallQueue);
}
public boolean onScaleChangedCalled() {
return mCallQueue.size() > 0;
}
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// We know the CtsTestServer gave us fake credential, so we ignore the SSL error.
handler.proceed();
}
}
}