Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright 2019 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.webkit.cts;
     18 
     19 import static org.hamcrest.MatcherAssert.assertThat;
     20 import static org.hamcrest.Matchers.greaterThan;
     21 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
     22 import static org.hamcrest.Matchers.lessThan;
     23 import static org.hamcrest.Matchers.lessThanOrEqualTo;
     24 
     25 import android.net.http.SslError;
     26 import android.os.StrictMode;
     27 import android.os.StrictMode.ThreadPolicy;
     28 import android.platform.test.annotations.AppModeFull;
     29 import android.test.ActivityInstrumentationTestCase2;
     30 import android.webkit.SslErrorHandler;
     31 import android.webkit.WebSettings;
     32 import android.webkit.WebView;
     33 import android.webkit.WebViewClient;
     34 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
     35 
     36 import com.android.compatibility.common.util.NullWebViewUtils;
     37 import com.android.compatibility.common.util.PollingCheck;
     38 
     39 import java.util.concurrent.BlockingQueue;
     40 import java.util.concurrent.LinkedBlockingQueue;
     41 
     42 /**
     43  *  Test WebView zooming behaviour
     44  */
     45 @AppModeFull
     46 public class WebViewZoomTest extends ActivityInstrumentationTestCase2<WebViewCtsActivity> {
     47     private WebView mWebView;
     48     private WebViewOnUiThread mOnUiThread;
     49     private ScaleChangedWebViewClient mWebViewClient;
     50     private CtsTestServer mWebServer;
     51 
     52     /**
     53      * Epsilon used in page scale value comparisons.
     54      */
     55     private static final float PAGE_SCALE_EPSILON = 0.0001f;
     56 
     57     public WebViewZoomTest() {
     58         super("com.android.cts.webkit", WebViewCtsActivity.class);
     59     }
     60 
     61     @Override
     62     protected void setUp() throws Exception {
     63         super.setUp();
     64         final WebViewCtsActivity activity = getActivity();
     65         mWebView = activity.getWebView();
     66         if (mWebView == null)
     67             return;
     68         mOnUiThread = new WebViewOnUiThread(mWebView);
     69         mOnUiThread.requestFocus();
     70 
     71         new PollingCheck() {
     72             @Override
     73                 protected boolean check() {
     74                     return activity.hasWindowFocus();
     75             }
     76         }.run();
     77 
     78         mWebViewClient = new ScaleChangedWebViewClient();
     79         mOnUiThread.setWebViewClient(mWebViewClient);
     80 
     81         // Pinch zoom is not supported in wrap_content layouts.
     82         mOnUiThread.setLayoutHeightToMatchParent();
     83 
     84     }
     85 
     86     @Override
     87     protected void tearDown() throws Exception {
     88         if (mOnUiThread != null) {
     89             mOnUiThread.cleanUp();
     90         }
     91         if (mWebServer != null) {
     92             stopWebServer();
     93         }
     94         super.tearDown();
     95     }
     96 
     97     private void stopWebServer() throws Exception {
     98         assertNotNull(mWebServer);
     99         ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
    100         ThreadPolicy tmpPolicy = new ThreadPolicy.Builder(oldPolicy)
    101                 .permitNetwork()
    102                 .build();
    103         StrictMode.setThreadPolicy(tmpPolicy);
    104         mWebServer.shutdown();
    105         mWebServer = null;
    106         StrictMode.setThreadPolicy(oldPolicy);
    107     }
    108 
    109     private void setUpPage() throws Exception {
    110         assertFalse(mWebViewClient.onScaleChangedCalled());
    111         assertNull(mWebServer);
    112         // Pass CtsTestserver.SslMode.TRUST_ANY_CLIENT to make the server serve https URLs yet do
    113         // not ask client for client authentication.
    114         mWebServer = new CtsTestServer(getActivity(), CtsTestServer.SslMode.TRUST_ANY_CLIENT);
    115         mOnUiThread.loadUrlAndWaitForCompletion(
    116                 mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
    117         pollingCheckForCanZoomIn();
    118     }
    119 
    120     public void testZoomIn() throws Throwable {
    121         if (!NullWebViewUtils.isWebViewAvailable()) {
    122             return;
    123         }
    124 
    125         setUpPage();
    126 
    127         assertTrue(mOnUiThread.zoomIn());
    128         mWebViewClient.waitForNextScaleChange();
    129     }
    130 
    131     @SuppressWarnings("deprecation")
    132     public void testGetZoomControls() {
    133         if (!NullWebViewUtils.isWebViewAvailable()) {
    134             return;
    135         }
    136         WebSettings settings = mOnUiThread.getSettings();
    137         assertTrue(settings.supportZoom());
    138         assertNotNull(
    139                 "Should be able to get zoom controls when zoom is enabled",
    140                 WebkitUtils.onMainThreadSync(() -> { return mWebView.getZoomControls(); }));
    141 
    142         // disable zoom support
    143         settings.setSupportZoom(false);
    144         assertFalse(settings.supportZoom());
    145         assertNull(
    146                 "Should not be able to get zoom controls when zoom is disabled",
    147                 WebkitUtils.onMainThreadSync(() -> { return mWebView.getZoomControls(); }));
    148     }
    149 
    150     public void testInvokeZoomPicker() throws Exception {
    151         if (!NullWebViewUtils.isWebViewAvailable()) {
    152             return;
    153         }
    154         WebSettings settings = mOnUiThread.getSettings();
    155         assertTrue(settings.supportZoom());
    156         setUpPage();
    157         WebkitUtils.onMainThreadSync(() -> mWebView.invokeZoomPicker());
    158     }
    159 
    160     public void testZoom_canNotZoomInPastMaximum() {
    161         if (!NullWebViewUtils.isWebViewAvailable()) {
    162             return;
    163         }
    164         float currScale = mOnUiThread.getScale();
    165         // Zoom in until maximum scale, in default increments.
    166         while (mOnUiThread.zoomIn()) {
    167             currScale = mWebViewClient.expectZoomIn(currScale);
    168         }
    169 
    170         assertFalse(mOnUiThread.zoomIn());
    171         assertNoScaleChange(currScale);
    172     }
    173 
    174     public void testZoom_canNotZoomOutPastMinimum() {
    175         if (!NullWebViewUtils.isWebViewAvailable()) {
    176             return;
    177         }
    178         float currScale = mOnUiThread.getScale();
    179         // Zoom in until maximum scale, in default increments.
    180         while (mOnUiThread.zoomOut()) {
    181             currScale = mWebViewClient.expectZoomOut(currScale);
    182         }
    183 
    184         assertFalse(mOnUiThread.zoomOut());
    185         assertNoScaleChange(currScale);
    186     }
    187 
    188     public void testCanZoomWhileZoomSupportedFalse() throws Throwable {
    189         // setZoomSupported disables user controls, but not zooming via API
    190         if (!NullWebViewUtils.isWebViewAvailable()) {
    191             return;
    192         }
    193 
    194         setUpPage();
    195 
    196         WebSettings settings = mOnUiThread.getSettings();
    197         settings.setSupportZoom(false);
    198         assertFalse(settings.supportZoom());
    199 
    200         float currScale = mOnUiThread.getScale();
    201 
    202         assertTrue("Zoom out should succeed although zoom support is disabled in web settings",
    203                 mOnUiThread.zoomIn());
    204         currScale = mWebViewClient.expectZoomIn(currScale);
    205 
    206         assertTrue("Zoom out should succeed although zoom support is disabled in web settings",
    207                 mOnUiThread.zoomOut());
    208         currScale = mWebViewClient.expectZoomOut(currScale);
    209     }
    210 
    211     public void testZoomByPowerOfTwoIncrements() throws Throwable {
    212         // setZoomSupported disables user controls, but not zooming via API
    213         if (!NullWebViewUtils.isWebViewAvailable()) {
    214             return;
    215         }
    216 
    217         setUpPage();
    218         float currScale = mOnUiThread.getScale();
    219 
    220         mOnUiThread.zoomBy(1.25f); // zoom in
    221         currScale = mWebViewClient.expectZoomBy(currScale, 1.25f);
    222 
    223         mOnUiThread.zoomBy(0.75f); // zoom out
    224         currScale = mWebViewClient.expectZoomBy(currScale, 0.75f);
    225     }
    226 
    227     public void testZoomByNonPowerOfTwoIncrements() throws Throwable {
    228         if (!NullWebViewUtils.isWebViewAvailable()) {
    229             return;
    230         }
    231 
    232         setUpPage();
    233 
    234         float currScale = mOnUiThread.getScale();
    235 
    236         // Zoom in until maximum scale, in specified increments designed so that the last zoom will
    237         // be less than expected.
    238         while (mOnUiThread.canZoomIn()) {
    239             mOnUiThread.zoomBy(1.7f);
    240             currScale = mWebViewClient.expectZoomBy(currScale, 1.7f);
    241         }
    242 
    243         // At this point, zooming in should do nothing.
    244         mOnUiThread.zoomBy(1.7f);
    245         assertNoScaleChange(currScale);
    246 
    247         // Zoom out until minimum scale, in specified increments designed so that the last zoom will
    248         // be less than requested.
    249         while (mOnUiThread.canZoomOut()) {
    250             mOnUiThread.zoomBy(0.7f);
    251             currScale = mWebViewClient.expectZoomBy(currScale, 0.7f);
    252         }
    253 
    254         // At this point, zooming out should do nothing.
    255         mOnUiThread.zoomBy(0.7f);
    256         assertNoScaleChange(currScale);
    257     }
    258 
    259     public void testScaleChangeCallbackMatchesGetScale() throws Throwable {
    260         if (!NullWebViewUtils.isWebViewAvailable()) {
    261             return;
    262         }
    263         assertFalse(mWebViewClient.onScaleChangedCalled());
    264 
    265         setUpPage();
    266 
    267         assertFalse(mWebViewClient.onScaleChangedCalled());
    268         assertTrue(mOnUiThread.zoomIn());
    269         ScaleChangedState state = mWebViewClient.waitForNextScaleChange();
    270         assertEquals(
    271                 "Expected onScaleChanged arg 2 (new scale) to equal view.getScale()",
    272                 state.mNewScale, mOnUiThread.getScale());
    273     }
    274 
    275     private void assertNoScaleChange(float currScale) {
    276         // We sleep to assert to the best of our ability
    277         // that a scale change does *not* happen.
    278         try {
    279             Thread.sleep(500);
    280             assertFalse(mWebViewClient.onScaleChangedCalled());
    281             assertEquals(currScale, mOnUiThread.getScale());
    282         } catch (InterruptedException e) {
    283             fail("Interrupted");
    284         }
    285     }
    286 
    287     private static final class ScaleChangedState {
    288         public float mOldScale;
    289         public float mNewScale;
    290         public boolean mCanZoomIn;
    291         public boolean mCanZoomOut;
    292 
    293         ScaleChangedState(WebView view, float oldScale, float newScale) {
    294             mOldScale = oldScale;
    295             mNewScale = newScale;
    296             mCanZoomIn = view.canZoomIn();
    297             mCanZoomOut = view.canZoomOut();
    298         }
    299     }
    300 
    301     private void pollingCheckForCanZoomIn() {
    302         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
    303             @Override
    304             protected boolean check() {
    305                 return mOnUiThread.canZoomIn();
    306             }
    307         }.run();
    308     }
    309 
    310     private final class ScaleChangedWebViewClient extends WaitForLoadedClient {
    311         private BlockingQueue<ScaleChangedState> mCallQueue;
    312 
    313         public ScaleChangedWebViewClient() {
    314             super(mOnUiThread);
    315             mCallQueue = new LinkedBlockingQueue<>();
    316         }
    317 
    318         @Override
    319         public void onScaleChanged(WebView view, float oldScale, float newScale) {
    320             super.onScaleChanged(view, oldScale, newScale);
    321             mCallQueue.add(new ScaleChangedState(view, oldScale, newScale));
    322         }
    323 
    324         public float expectZoomBy(float currentScale, float scaleAmount) {
    325             assertTrue(scaleAmount != 1.0f);
    326 
    327             float nextScale = currentScale * scaleAmount;
    328             ScaleChangedState state = waitForNextScaleChange();
    329             assertEquals(currentScale, state.mOldScale);
    330 
    331             // Check that we zoomed in the expected direction wrt. the current scale.
    332             if (scaleAmount > 1.0f) {
    333                 assertThat(
    334                         "Expected new scale > current scale when zooming in",
    335                         state.mNewScale, greaterThan(currentScale));
    336             } else {
    337                 assertThat(
    338                         "Expected new scale < current scale when zooming out",
    339                         state.mNewScale, lessThan(currentScale));
    340             }
    341 
    342             // If we hit the zoom limit, then the new scale should be between the old scale
    343             // and the expected new scale. Otherwise it should equal the expected new scale.
    344             if (Math.abs(nextScale - state.mNewScale) > PAGE_SCALE_EPSILON) {
    345                 if (scaleAmount > 1.0f) {
    346                     assertFalse(state.mCanZoomIn);
    347                     assertThat(
    348                             "Expected new scale <= requested scale when zooming in past limit",
    349                             state.mNewScale, lessThanOrEqualTo(nextScale));
    350                 } else {
    351                     assertFalse(state.mCanZoomOut);
    352                     assertThat(
    353                             "Expected new scale >= requested scale when zooming out past limit",
    354                             state.mNewScale, greaterThanOrEqualTo(nextScale));
    355                 }
    356             }
    357 
    358             return state.mNewScale;
    359         }
    360 
    361         public float expectZoomOut(float currentScale) {
    362             ScaleChangedState state = waitForNextScaleChange();
    363             assertEquals(currentScale, state.mOldScale);
    364             assertThat(state.mNewScale, lessThan(currentScale));
    365             return state.mNewScale;
    366         }
    367 
    368         public float expectZoomIn(float currentScale) {
    369             ScaleChangedState state = waitForNextScaleChange();
    370             assertEquals(currentScale, state.mOldScale);
    371             assertThat(state.mNewScale, greaterThan(currentScale));
    372             return state.mNewScale;
    373         }
    374 
    375         public ScaleChangedState waitForNextScaleChange() {
    376             return WebkitUtils.waitForNextQueueElement(mCallQueue);
    377         }
    378 
    379         public boolean onScaleChangedCalled() {
    380             return mCallQueue.size() > 0;
    381         }
    382 
    383         public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    384             // We know the CtsTestServer gave us fake credential, so we ignore the SSL error.
    385             handler.proceed();
    386         }
    387     }
    388 }
    389