1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.android_webview.test;
6
7import android.content.Context;
8import android.test.suitebuilder.annotation.SmallTest;
9import android.view.View;
10import android.widget.OverScroller;
11
12import org.chromium.android_webview.AwContents;
13import org.chromium.android_webview.AwScrollOffsetManager;
14import org.chromium.android_webview.test.util.AwTestTouchUtils;
15import org.chromium.android_webview.test.util.CommonResources;
16import org.chromium.android_webview.test.util.JavascriptEventObserver;
17import org.chromium.base.test.util.Feature;
18import org.chromium.content.browser.test.util.CallbackHelper;
19import org.chromium.content_public.browser.GestureStateListener;
20import org.chromium.ui.gfx.DeviceDisplayInfo;
21
22import java.util.Locale;
23import java.util.concurrent.Callable;
24import java.util.concurrent.CountDownLatch;
25import java.util.concurrent.atomic.AtomicBoolean;
26
27/**
28 * Integration tests for synchronous scrolling.
29 */
30public class AndroidScrollIntegrationTest extends AwTestBase {
31    private static class OverScrollByCallbackHelper extends CallbackHelper {
32        int mDeltaX;
33        int mDeltaY;
34        int mScrollRangeY;
35
36        public int getDeltaX() {
37            assert getCallCount() > 0;
38            return mDeltaX;
39        }
40
41        public int getDeltaY() {
42            assert getCallCount() > 0;
43            return mDeltaY;
44        }
45
46        public int getScrollRangeY() {
47            assert getCallCount() > 0;
48            return mScrollRangeY;
49        }
50
51        public void notifyCalled(int deltaX, int deltaY, int scrollRangeY) {
52            mDeltaX = deltaX;
53            mDeltaY = deltaY;
54            mScrollRangeY = scrollRangeY;
55            notifyCalled();
56        }
57    }
58
59    private static class ScrollTestContainerView extends AwTestContainerView {
60        private int mMaxScrollXPix = -1;
61        private int mMaxScrollYPix = -1;
62
63        private CallbackHelper mOnScrollToCallbackHelper = new CallbackHelper();
64        private OverScrollByCallbackHelper mOverScrollByCallbackHelper =
65            new OverScrollByCallbackHelper();
66
67        public ScrollTestContainerView(Context context) {
68            super(context, false);
69        }
70
71        public CallbackHelper getOnScrollToCallbackHelper() {
72            return mOnScrollToCallbackHelper;
73        }
74
75        public OverScrollByCallbackHelper getOverScrollByCallbackHelper() {
76            return mOverScrollByCallbackHelper;
77        }
78
79        public void setMaxScrollX(int maxScrollXPix) {
80            mMaxScrollXPix = maxScrollXPix;
81        }
82
83        public void setMaxScrollY(int maxScrollYPix) {
84            mMaxScrollYPix = maxScrollYPix;
85        }
86
87        @Override
88        protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY,
89                     int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY,
90                     boolean isTouchEvent) {
91            mOverScrollByCallbackHelper.notifyCalled(deltaX, deltaY, scrollRangeY);
92            return super.overScrollBy(deltaX, deltaY, scrollX, scrollY,
93                     scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
94        }
95
96        @Override
97        public void scrollTo(int x, int y) {
98            if (mMaxScrollXPix != -1)
99                x = Math.min(mMaxScrollXPix, x);
100            if (mMaxScrollYPix != -1)
101                y = Math.min(mMaxScrollYPix, y);
102            super.scrollTo(x, y);
103            mOnScrollToCallbackHelper.notifyCalled();
104        }
105    }
106
107    @Override
108    protected TestDependencyFactory createTestDependencyFactory() {
109        return new TestDependencyFactory() {
110            @Override
111            public AwScrollOffsetManager createScrollOffsetManager(
112                    AwScrollOffsetManager.Delegate delegate, OverScroller overScroller) {
113                return new AwScrollOffsetManager(delegate, overScroller) {
114                    @Override
115                    public void onUnhandledFlingStartEvent(int velocityX, int velocityY) {
116                        // Intentional no-op. The synthetic scroll gestures this test creates all
117                        // happen at the same time which triggers the fling detection logic.
118                        // NOTE: this simply disables handling the gesture, flinging the AwContents
119                        // via the flingScroll API is still possible.
120                    }
121                };
122            }
123            @Override
124            public AwTestContainerView createAwTestContainerView(AwTestRunnerActivity activity) {
125                return new ScrollTestContainerView(activity);
126            }
127        };
128    }
129
130    private static final String TEST_PAGE_COMMON_HEADERS =
131        "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> " +
132        "<style type=\"text/css\"> " +
133        "   body { " +
134        "      margin: 0px; " +
135        "   } " +
136        "   div { " +
137        "      width:1000px; " +
138        "      height:10000px; " +
139        "      background-color: blue; " +
140        "   } " +
141        "</style> ";
142    private static final String TEST_PAGE_COMMON_CONTENT = "<div>test div</div> ";
143
144    private String makeTestPage(String onscrollObserver, String firstFrameObserver,
145            String extraContent) {
146        String content = TEST_PAGE_COMMON_CONTENT + extraContent;
147        if (onscrollObserver != null) {
148            content +=
149                    "<script> " +
150                    "   window.onscroll = function(oEvent) { " +
151                    "       " + onscrollObserver + ".notifyJava(); " +
152                    "   } " +
153                    "</script>";
154        }
155        if (firstFrameObserver != null) {
156            content +=
157                    "<script> " +
158                    "   window.framesToIgnore = 20; " +
159                    "   window.onAnimationFrame = function(timestamp) { " +
160                    "     if (window.framesToIgnore == 0) { " +
161                    "         " + firstFrameObserver + ".notifyJava(); " +
162                    "     } else {" +
163                    "       window.framesToIgnore -= 1; " +
164                    "       window.requestAnimationFrame(window.onAnimationFrame); " +
165                    "     } " +
166                    "   }; " +
167                    "   window.requestAnimationFrame(window.onAnimationFrame); " +
168                    "</script>";
169        }
170        return CommonResources.makeHtmlPageFrom(TEST_PAGE_COMMON_HEADERS, content);
171    }
172
173    private void scrollToOnMainSync(final View view, final int xPix, final int yPix) {
174        getInstrumentation().runOnMainSync(new Runnable() {
175            @Override
176            public void run() {
177                view.scrollTo(xPix, yPix);
178            }
179        });
180    }
181
182    private void setMaxScrollOnMainSync(final ScrollTestContainerView testContainerView,
183            final int maxScrollXPix, final int maxScrollYPix) {
184        getInstrumentation().runOnMainSync(new Runnable() {
185            @Override
186            public void run() {
187                testContainerView.setMaxScrollX(maxScrollXPix);
188                testContainerView.setMaxScrollY(maxScrollYPix);
189            }
190        });
191    }
192
193    private boolean checkScrollOnMainSync(final ScrollTestContainerView testContainerView,
194            final int scrollXPix, final int scrollYPix) {
195        final AtomicBoolean equal = new AtomicBoolean(false);
196        getInstrumentation().runOnMainSync(new Runnable() {
197            @Override
198            public void run() {
199                equal.set((scrollXPix == testContainerView.getScrollX()) &&
200                    (scrollYPix == testContainerView.getScrollY()));
201            }
202        });
203        return equal.get();
204    }
205
206    private void assertScrollOnMainSync(final ScrollTestContainerView testContainerView,
207            final int scrollXPix, final int scrollYPix) {
208        getInstrumentation().runOnMainSync(new Runnable() {
209            @Override
210            public void run() {
211                assertEquals(scrollXPix, testContainerView.getScrollX());
212                assertEquals(scrollYPix, testContainerView.getScrollY());
213            }
214        });
215    }
216
217    private void assertScrollInJs(final AwContents awContents,
218            final TestAwContentsClient contentsClient,
219            final int xCss, final int yCss) throws Exception {
220        poll(new Callable<Boolean>() {
221            @Override
222            public Boolean call() throws Exception {
223                String x = executeJavaScriptAndWaitForResult(awContents, contentsClient,
224                    "window.scrollX");
225                String y = executeJavaScriptAndWaitForResult(awContents, contentsClient,
226                    "window.scrollY");
227                return (Integer.toString(xCss).equals(x) &&
228                    Integer.toString(yCss).equals(y));
229            }
230        });
231    }
232
233    private void assertScrolledToBottomInJs(final AwContents awContents,
234            final TestAwContentsClient contentsClient) throws Exception {
235        final String isBottomScript = "window.scrollY == " +
236            "(window.document.documentElement.scrollHeight - window.innerHeight)";
237        poll(new Callable<Boolean>() {
238            @Override
239            public Boolean call() throws Exception {
240                String r = executeJavaScriptAndWaitForResult(awContents, contentsClient,
241                    isBottomScript);
242                return r.equals("true");
243            }
244        });
245    }
246
247    private void loadTestPageAndWaitForFirstFrame(final ScrollTestContainerView testContainerView,
248            final TestAwContentsClient contentsClient,
249            final String onscrollObserverName, final String extraContent) throws Exception {
250        final JavascriptEventObserver firstFrameObserver = new JavascriptEventObserver();
251        final String firstFrameObserverName = "firstFrameObserver";
252        enableJavaScriptOnUiThread(testContainerView.getAwContents());
253
254        getInstrumentation().runOnMainSync(new Runnable() {
255            @Override
256            public void run() {
257                firstFrameObserver.register(testContainerView.getContentViewCore(),
258                    firstFrameObserverName);
259            }
260        });
261
262        loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(),
263                makeTestPage(onscrollObserverName, firstFrameObserverName, extraContent),
264                "text/html", false);
265
266        // We wait for "a couple" of frames for the active tree in CC to stabilize and for pending
267        // tree activations to stop clobbering the root scroll layer's scroll offset. This wait
268        // doesn't strictly guarantee that but there isn't a good alternative and this seems to
269        // work fine.
270        firstFrameObserver.waitForEvent(WAIT_TIMEOUT_MS);
271    }
272
273    @SmallTest
274    @Feature({"AndroidWebView"})
275    public void testUiScrollReflectedInJs() throws Throwable {
276        final TestAwContentsClient contentsClient = new TestAwContentsClient();
277        final ScrollTestContainerView testContainerView =
278            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
279        enableJavaScriptOnUiThread(testContainerView.getAwContents());
280
281        final double deviceDIPScale =
282            DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
283        final int targetScrollXCss = 233;
284        final int targetScrollYCss = 322;
285        final int targetScrollXPix = (int) Math.ceil(targetScrollXCss * deviceDIPScale);
286        final int targetScrollYPix = (int) Math.ceil(targetScrollYCss * deviceDIPScale);
287        final JavascriptEventObserver onscrollObserver = new JavascriptEventObserver();
288
289        getInstrumentation().runOnMainSync(new Runnable() {
290            @Override
291            public void run() {
292                onscrollObserver.register(testContainerView.getContentViewCore(),
293                    "onscrollObserver");
294            }
295        });
296
297        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, "onscrollObserver", "");
298
299        scrollToOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix);
300
301        onscrollObserver.waitForEvent(WAIT_TIMEOUT_MS);
302        assertScrollInJs(testContainerView.getAwContents(), contentsClient,
303                targetScrollXCss, targetScrollYCss);
304    }
305
306    @SmallTest
307    @Feature({"AndroidWebView"})
308    public void testJsScrollReflectedInUi() throws Throwable {
309        final TestAwContentsClient contentsClient = new TestAwContentsClient();
310        final ScrollTestContainerView testContainerView =
311            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
312        enableJavaScriptOnUiThread(testContainerView.getAwContents());
313
314        final double deviceDIPScale =
315            DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
316        final int targetScrollXCss = 132;
317        final int targetScrollYCss = 243;
318        final int targetScrollXPix = (int) Math.floor(targetScrollXCss * deviceDIPScale);
319        final int targetScrollYPix = (int) Math.floor(targetScrollYCss * deviceDIPScale);
320
321        loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(),
322                makeTestPage(null, null, ""), "text/html", false);
323
324        final CallbackHelper onScrollToCallbackHelper =
325            testContainerView.getOnScrollToCallbackHelper();
326        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
327        executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient,
328                String.format("window.scrollTo(%d, %d);", targetScrollXCss, targetScrollYCss));
329        onScrollToCallbackHelper.waitForCallback(scrollToCallCount);
330
331        assertScrollOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix);
332    }
333
334    @SmallTest
335    @Feature({"AndroidWebView"})
336    public void testJsScrollFromBody() throws Throwable {
337        final TestAwContentsClient contentsClient = new TestAwContentsClient();
338        final ScrollTestContainerView testContainerView =
339            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
340        enableJavaScriptOnUiThread(testContainerView.getAwContents());
341
342        final double deviceDIPScale =
343            DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
344        final int targetScrollXCss = 132;
345        final int targetScrollYCss = 243;
346        final int targetScrollXPix = (int) Math.floor(targetScrollXCss * deviceDIPScale);
347        final int targetScrollYPix = (int) Math.floor(targetScrollYCss * deviceDIPScale);
348
349        final String scrollFromBodyScript =
350            "<script> " +
351            "  window.scrollTo(" + targetScrollXCss + ", " + targetScrollYCss + "); " +
352            "</script> ";
353
354        final CallbackHelper onScrollToCallbackHelper =
355            testContainerView.getOnScrollToCallbackHelper();
356        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
357        loadDataAsync(testContainerView.getAwContents(),
358                makeTestPage(null, null, scrollFromBodyScript), "text/html", false);
359        onScrollToCallbackHelper.waitForCallback(scrollToCallCount);
360
361        assertScrollOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix);
362    }
363
364    @SmallTest
365    @Feature({"AndroidWebView"})
366    public void testJsScrollCanBeAlteredByUi() throws Throwable {
367        final TestAwContentsClient contentsClient = new TestAwContentsClient();
368        final ScrollTestContainerView testContainerView =
369            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
370        enableJavaScriptOnUiThread(testContainerView.getAwContents());
371
372        final double deviceDIPScale =
373            DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
374        final int targetScrollXCss = 132;
375        final int targetScrollYCss = 243;
376        final int targetScrollXPix = (int) Math.floor(targetScrollXCss * deviceDIPScale);
377        final int targetScrollYPix = (int) Math.floor(targetScrollYCss * deviceDIPScale);
378
379        final int maxScrollXCss = 101;
380        final int maxScrollYCss = 201;
381        final int maxScrollXPix = (int) Math.floor(maxScrollXCss * deviceDIPScale);
382        final int maxScrollYPix = (int) Math.floor(maxScrollYCss * deviceDIPScale);
383
384        loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(),
385                makeTestPage(null, null, ""), "text/html", false);
386
387        setMaxScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix);
388
389        final CallbackHelper onScrollToCallbackHelper =
390            testContainerView.getOnScrollToCallbackHelper();
391        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
392        executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient,
393                "window.scrollTo(" + targetScrollXCss + "," + targetScrollYCss + ")");
394        onScrollToCallbackHelper.waitForCallback(scrollToCallCount);
395
396        assertScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix);
397    }
398
399    @SmallTest
400    @Feature({"AndroidWebView"})
401    public void testTouchScrollCanBeAlteredByUi() throws Throwable {
402        final TestAwContentsClient contentsClient = new TestAwContentsClient();
403        final ScrollTestContainerView testContainerView =
404            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
405        enableJavaScriptOnUiThread(testContainerView.getAwContents());
406
407        final int dragSteps = 10;
408        final int dragStepSize = 24;
409        // Watch out when modifying - if the y or x delta aren't big enough vertical or horizontal
410        // scroll snapping will kick in.
411        final int targetScrollXPix = dragStepSize * dragSteps;
412        final int targetScrollYPix = dragStepSize * dragSteps;
413
414        final double deviceDIPScale =
415            DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
416        final int maxScrollXPix = 101;
417        final int maxScrollYPix = 211;
418        // Make sure we can't hit these values simply as a result of scrolling.
419        assert (maxScrollXPix % dragStepSize) != 0;
420        assert (maxScrollYPix % dragStepSize) != 0;
421        final int maxScrollXCss = (int) Math.floor(maxScrollXPix / deviceDIPScale);
422        final int maxScrollYCss = (int) Math.floor(maxScrollYPix / deviceDIPScale);
423
424        setMaxScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix);
425
426        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
427
428        final CallbackHelper onScrollToCallbackHelper =
429            testContainerView.getOnScrollToCallbackHelper();
430        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
431        AwTestTouchUtils.dragCompleteView(testContainerView,
432                0, -targetScrollXPix, // these need to be negative as we're scrolling down.
433                0, -targetScrollYPix,
434                dragSteps,
435                null /* completionLatch */);
436
437        for (int i = 1; i <= dragSteps; ++i) {
438            onScrollToCallbackHelper.waitForCallback(scrollToCallCount, i);
439            if (checkScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix))
440                break;
441        }
442
443        assertScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix);
444        assertScrollInJs(testContainerView.getAwContents(), contentsClient,
445                maxScrollXCss, maxScrollYCss);
446    }
447
448    @SmallTest
449    @Feature({"AndroidWebView"})
450    public void testNoSpuriousOverScrolls() throws Throwable {
451        final TestAwContentsClient contentsClient = new TestAwContentsClient();
452        final ScrollTestContainerView testContainerView =
453            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
454        enableJavaScriptOnUiThread(testContainerView.getAwContents());
455
456        final int dragSteps = 1;
457        final int targetScrollYPix = 40;
458
459        setMaxScrollOnMainSync(testContainerView, 0, 0);
460
461        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
462
463        final CallbackHelper onScrollToCallbackHelper =
464            testContainerView.getOnScrollToCallbackHelper();
465        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
466        CountDownLatch scrollingCompleteLatch = new CountDownLatch(1);
467        AwTestTouchUtils.dragCompleteView(testContainerView,
468                0, 0, // these need to be negative as we're scrolling down.
469                0, -targetScrollYPix,
470                dragSteps,
471                scrollingCompleteLatch);
472        try {
473            scrollingCompleteLatch.await();
474        } catch (InterruptedException ex) {
475            // ignore
476        }
477        assertEquals(scrollToCallCount + 1, onScrollToCallbackHelper.getCallCount());
478    }
479
480    @SmallTest
481    @Feature({"AndroidWebView"})
482    public void testOverScrollX() throws Throwable {
483        final TestAwContentsClient contentsClient = new TestAwContentsClient();
484        final ScrollTestContainerView testContainerView =
485            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
486        final OverScrollByCallbackHelper overScrollByCallbackHelper =
487            testContainerView.getOverScrollByCallbackHelper();
488        enableJavaScriptOnUiThread(testContainerView.getAwContents());
489
490        final int overScrollDeltaX = 30;
491        final int oneStep = 1;
492
493        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
494
495        // Scroll separately in different dimensions because of vertical/horizontal scroll
496        // snap.
497        final int overScrollCallCount = overScrollByCallbackHelper.getCallCount();
498        AwTestTouchUtils.dragCompleteView(testContainerView,
499                0, overScrollDeltaX,
500                0, 0,
501                oneStep,
502                null /* completionLatch */);
503        overScrollByCallbackHelper.waitForCallback(overScrollCallCount);
504        // Unfortunately the gesture detector seems to 'eat' some number of pixels. For now
505        // checking that the value is < 0 (overscroll is reported as negative values) will have to
506        // do.
507        assertTrue(0 > overScrollByCallbackHelper.getDeltaX());
508        assertEquals(0, overScrollByCallbackHelper.getDeltaY());
509
510        assertScrollOnMainSync(testContainerView, 0, 0);
511    }
512
513    @SmallTest
514    @Feature({"AndroidWebView"})
515    public void testOverScrollY() throws Throwable {
516        final TestAwContentsClient contentsClient = new TestAwContentsClient();
517        final ScrollTestContainerView testContainerView =
518            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
519        final OverScrollByCallbackHelper overScrollByCallbackHelper =
520            testContainerView.getOverScrollByCallbackHelper();
521        enableJavaScriptOnUiThread(testContainerView.getAwContents());
522
523        final int overScrollDeltaY = 30;
524        final int oneStep = 1;
525
526        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
527
528        int overScrollCallCount = overScrollByCallbackHelper.getCallCount();
529        AwTestTouchUtils.dragCompleteView(testContainerView,
530                0, 0,
531                0, overScrollDeltaY,
532                oneStep,
533                null /* completionLatch */);
534        overScrollByCallbackHelper.waitForCallback(overScrollCallCount);
535        assertEquals(0, overScrollByCallbackHelper.getDeltaX());
536        assertTrue(0 > overScrollByCallbackHelper.getDeltaY());
537
538        assertScrollOnMainSync(testContainerView, 0, 0);
539    }
540
541    @SmallTest
542    @Feature({"AndroidWebView"})
543    public void testScrollToBottomAtPageScaleX0dot5() throws Throwable {
544        // The idea behind this test is to check that scrolling to the bottom on ther renderer side
545        // results in the view also reporting as being scrolled to the bottom.
546        final TestAwContentsClient contentsClient = new TestAwContentsClient();
547        final ScrollTestContainerView testContainerView =
548            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
549        enableJavaScriptOnUiThread(testContainerView.getAwContents());
550
551        final int targetScrollXCss = 1000;
552        final int targetScrollYCss = 10000;
553
554        final String pageHeaders =
555            "<meta name=\"viewport\" content=\"width=device-width, initial-scale=0.6\"> " +
556            "<style type=\"text/css\"> " +
557            "   div { " +
558            "      width:1000px; " +
559            "      height:10000px; " +
560            "      background-color: blue; " +
561            "   } " +
562            "   body { " +
563            "      margin: 0px; " +
564            "      padding: 0px; " +
565            "   } " +
566            "</style> ";
567
568        loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(),
569                CommonResources.makeHtmlPageFrom(pageHeaders, TEST_PAGE_COMMON_CONTENT),
570                "text/html", false);
571
572        final double deviceDIPScale =
573            DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
574
575        final CallbackHelper onScrollToCallbackHelper =
576            testContainerView.getOnScrollToCallbackHelper();
577        int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
578        executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient,
579                "window.scrollTo(" + targetScrollXCss + "," + targetScrollYCss + ")");
580        onScrollToCallbackHelper.waitForCallback(scrollToCallCount);
581
582        getInstrumentation().runOnMainSync(new Runnable() {
583            @Override
584            public void run() {
585                AwContents awContents = testContainerView.getAwContents();
586                int maxHorizontal = awContents.computeHorizontalScrollRange() -
587                                testContainerView.getWidth();
588                int maxVertical = awContents.computeVerticalScrollRange() -
589                                testContainerView.getHeight();
590                // Due to rounding going from CSS -> physical pixels it is possible that more than
591                // one physical pixels corespond to one CSS pixel, which is why we can't do a
592                // simple equality test here.
593                assertTrue(maxHorizontal - awContents.computeHorizontalScrollOffset() < 3);
594                assertTrue(maxVertical - awContents.computeVerticalScrollOffset() < 3);
595            }
596        });
597
598        scrollToCallCount = onScrollToCallbackHelper.getCallCount();
599        executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient,
600                "window.scrollTo(0, 0)");
601        onScrollToCallbackHelper.waitForCallback(scrollToCallCount);
602
603        getInstrumentation().runOnMainSync(new Runnable() {
604            @Override
605            public void run() {
606                AwContents awContents = testContainerView.getAwContents();
607                int maxHorizontal = awContents.computeHorizontalScrollRange() -
608                                testContainerView.getWidth();
609                int maxVertical = awContents.computeVerticalScrollRange() -
610                                testContainerView.getHeight();
611                testContainerView.scrollTo(maxHorizontal, maxVertical);
612            }
613        });
614        assertScrolledToBottomInJs(testContainerView.getAwContents(), contentsClient);
615    }
616
617    @SmallTest
618    @Feature({"AndroidWebView"})
619    public void testFlingScroll() throws Throwable {
620        final TestAwContentsClient contentsClient = new TestAwContentsClient();
621        final ScrollTestContainerView testContainerView =
622            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
623        enableJavaScriptOnUiThread(testContainerView.getAwContents());
624
625        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
626
627        assertScrollOnMainSync(testContainerView, 0, 0);
628
629        final CallbackHelper onScrollToCallbackHelper =
630            testContainerView.getOnScrollToCallbackHelper();
631        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
632
633        getInstrumentation().runOnMainSync(new Runnable() {
634            @Override
635            public void run() {
636                testContainerView.getAwContents().flingScroll(1000, 1000);
637            }
638        });
639
640        onScrollToCallbackHelper.waitForCallback(scrollToCallCount);
641
642        getInstrumentation().runOnMainSync(new Runnable() {
643            @Override
644            public void run() {
645                assertTrue(testContainerView.getScrollX() > 0);
646                assertTrue(testContainerView.getScrollY() > 0);
647            }
648        });
649    }
650
651    @SmallTest
652    @Feature({"AndroidWebView"})
653    public void testPageDown() throws Throwable {
654        final TestAwContentsClient contentsClient = new TestAwContentsClient();
655        final ScrollTestContainerView testContainerView =
656            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
657        enableJavaScriptOnUiThread(testContainerView.getAwContents());
658
659        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
660
661        assertScrollOnMainSync(testContainerView, 0, 0);
662
663        final int maxScrollYPix = runTestOnUiThreadAndGetResult(new Callable<Integer>() {
664            @Override
665            public Integer call() {
666                return (testContainerView.getAwContents().computeVerticalScrollRange() -
667                    testContainerView.getHeight());
668            }
669        });
670
671        final CallbackHelper onScrollToCallbackHelper =
672            testContainerView.getOnScrollToCallbackHelper();
673        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
674
675        getInstrumentation().runOnMainSync(new Runnable() {
676            @Override
677            public void run() {
678                testContainerView.getAwContents().pageDown(true);
679            }
680        });
681
682        // Wait for the animation to hit the bottom of the page.
683        for (int i = 1;; ++i) {
684            onScrollToCallbackHelper.waitForCallback(scrollToCallCount, i);
685            if (checkScrollOnMainSync(testContainerView, 0, maxScrollYPix))
686                break;
687        }
688    }
689
690    @SmallTest
691    @Feature({"AndroidWebView"})
692    public void testPageUp() throws Throwable {
693        final TestAwContentsClient contentsClient = new TestAwContentsClient();
694        final ScrollTestContainerView testContainerView =
695            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
696        enableJavaScriptOnUiThread(testContainerView.getAwContents());
697
698        final double deviceDIPScale =
699            DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale();
700        final int targetScrollYCss = 243;
701        final int targetScrollYPix = (int) Math.ceil(targetScrollYCss * deviceDIPScale);
702
703        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
704
705        assertScrollOnMainSync(testContainerView, 0, 0);
706
707        scrollToOnMainSync(testContainerView, 0, targetScrollYPix);
708
709        final CallbackHelper onScrollToCallbackHelper =
710            testContainerView.getOnScrollToCallbackHelper();
711        final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
712
713        getInstrumentation().runOnMainSync(new Runnable() {
714            @Override
715            public void run() {
716                testContainerView.getAwContents().pageUp(true);
717            }
718        });
719
720        // Wait for the animation to hit the bottom of the page.
721        for (int i = 1;; ++i) {
722            onScrollToCallbackHelper.waitForCallback(scrollToCallCount, i);
723            if (checkScrollOnMainSync(testContainerView, 0, 0))
724                break;
725        }
726    }
727
728    private static class TestGestureStateListener extends GestureStateListener {
729        private CallbackHelper mOnScrollUpdateGestureConsumedHelper = new CallbackHelper();
730
731        public CallbackHelper getOnScrollUpdateGestureConsumedHelper() {
732            return mOnScrollUpdateGestureConsumedHelper;
733        }
734
735        @Override
736        public void onPinchStarted() {
737        }
738
739        @Override
740        public void onPinchEnded() {
741        }
742
743        @Override
744        public void onFlingStartGesture(
745                int velocityX, int velocityY, int scrollOffsetY, int scrollExtentY) {
746        }
747
748        @Override
749        public void onFlingCancelGesture() {
750        }
751
752        @Override
753        public void onUnhandledFlingStartEvent(int velocityX, int velocityY) {
754        }
755
756        @Override
757        public void onScrollUpdateGestureConsumed() {
758            mOnScrollUpdateGestureConsumedHelper.notifyCalled();
759        }
760    }
761
762    @SmallTest
763    @Feature({"AndroidWebView"})
764    public void testTouchScrollingConsumesScrollByGesture() throws Throwable {
765        final TestAwContentsClient contentsClient = new TestAwContentsClient();
766        final ScrollTestContainerView testContainerView =
767            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
768        final TestGestureStateListener testGestureStateListener = new TestGestureStateListener();
769        enableJavaScriptOnUiThread(testContainerView.getAwContents());
770
771        final int dragSteps = 10;
772        final int dragStepSize = 24;
773        // Watch out when modifying - if the y or x delta aren't big enough vertical or horizontal
774        // scroll snapping will kick in.
775        final int targetScrollXPix = dragStepSize * dragSteps;
776        final int targetScrollYPix = dragStepSize * dragSteps;
777
778        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null,
779                "<div>" +
780                "  <div style=\"width:10000px; height: 10000px;\"> force scrolling </div>" +
781                "</div>");
782
783        getInstrumentation().runOnMainSync(new Runnable() {
784            @Override
785            public void run() {
786                testContainerView.getContentViewCore().addGestureStateListener(
787                        testGestureStateListener);
788            }
789        });
790        final CallbackHelper onScrollUpdateGestureConsumedHelper =
791            testGestureStateListener.getOnScrollUpdateGestureConsumedHelper();
792
793        final int callCount = onScrollUpdateGestureConsumedHelper.getCallCount();
794        AwTestTouchUtils.dragCompleteView(testContainerView,
795                0, -targetScrollXPix, // these need to be negative as we're scrolling down.
796                0, -targetScrollYPix,
797                dragSteps,
798                null /* completionLatch */);
799        onScrollUpdateGestureConsumedHelper.waitForCallback(callCount);
800    }
801
802    @SmallTest
803    @Feature({"AndroidWebView"})
804    public void testPinchZoomUpdatesScrollRangeSynchronously() throws Throwable {
805        final TestAwContentsClient contentsClient = new TestAwContentsClient();
806        final ScrollTestContainerView testContainerView =
807            (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
808        final OverScrollByCallbackHelper overScrollByCallbackHelper =
809            testContainerView.getOverScrollByCallbackHelper();
810        final AwContents awContents = testContainerView.getAwContents();
811        enableJavaScriptOnUiThread(awContents);
812
813        loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, "");
814
815        getInstrumentation().runOnMainSync(new Runnable() {
816            @Override
817            public void run() {
818                assertTrue(awContents.canZoomIn());
819
820                int oldScrollRange =
821                    awContents.computeVerticalScrollRange() - testContainerView.getHeight();
822                float oldScale = awContents.getScale();
823                int oldContentHeightApproximation =
824                    (int) Math.ceil(awContents.computeVerticalScrollRange() / oldScale);
825
826                awContents.zoomIn();
827
828                int newScrollRange =
829                    awContents.computeVerticalScrollRange() - testContainerView.getHeight();
830                float newScale = awContents.getScale();
831                int newContentHeightApproximation =
832                    (int) Math.ceil(awContents.computeVerticalScrollRange() / newScale);
833
834                assertTrue(String.format(Locale.ENGLISH,
835                        "Scale range should increase after zoom (%f) > (%f)",
836                        newScale, oldScale), newScale > oldScale);
837                assertTrue(String.format(Locale.ENGLISH,
838                        "Scroll range should increase after zoom (%d) > (%d)",
839                        newScrollRange, oldScrollRange), newScrollRange > oldScrollRange);
840                assertEquals(awContents.getContentHeightCss(), oldContentHeightApproximation);
841                assertEquals(awContents.getContentHeightCss(), newContentHeightApproximation);
842            }
843        });
844
845    }
846}
847