// Copyright 2013 The Chromium Authors. All rights reserved. // 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.content.Context; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import android.view.Gravity; import android.view.View; import org.chromium.android_webview.AwContents; import org.chromium.android_webview.AwContentsClient; import org.chromium.android_webview.test.util.AwTestTouchUtils; import org.chromium.android_webview.test.util.CommonResources; import org.chromium.android_webview.test.util.JavascriptEventObserver; import org.chromium.base.test.util.DisabledTest; import org.chromium.base.test.util.Feature; import org.chromium.content.browser.ContentViewCore; import org.chromium.content.browser.test.util.CallbackHelper; import org.chromium.content.browser.test.util.Criteria; import org.chromium.content.browser.test.util.CriteriaHelper; import org.chromium.ui.gfx.DeviceDisplayInfo; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; /** * Integration tests for synchronous scrolling. */ public class AndroidScrollIntegrationTest extends AwTestBase { private static final int SCROLL_OFFSET_PROPAGATION_TIMEOUT_MS = 6 * 1000; private static class OverScrollByCallbackHelper extends CallbackHelper { int mDeltaX; int mDeltaY; public int getDeltaX() { assert getCallCount() > 0; return mDeltaX; } public int getDeltaY() { assert getCallCount() > 0; return mDeltaY; } public void notifyCalled(int deltaX, int deltaY) { mDeltaX = deltaX; mDeltaY = deltaY; notifyCalled(); } } private static class ScrollTestContainerView extends AwTestContainerView { private int mMaxScrollXPix = -1; private int mMaxScrollYPix = -1; private CallbackHelper mOnScrollToCallbackHelper = new CallbackHelper(); private OverScrollByCallbackHelper mOverScrollByCallbackHelper = new OverScrollByCallbackHelper(); public ScrollTestContainerView(Context context) { super(context); } public CallbackHelper getOnScrollToCallbackHelper() { return mOnScrollToCallbackHelper; } public OverScrollByCallbackHelper getOverScrollByCallbackHelper() { return mOverScrollByCallbackHelper; } public void setMaxScrollX(int maxScrollXPix) { mMaxScrollXPix = maxScrollXPix; } public void setMaxScrollY(int maxScrollYPix) { mMaxScrollYPix = maxScrollYPix; } @Override protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) { mOverScrollByCallbackHelper.notifyCalled(deltaX, deltaY); return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent); } @Override public void scrollTo(int x, int y) { if (mMaxScrollXPix != -1) x = Math.min(mMaxScrollXPix, x); if (mMaxScrollYPix != -1) y = Math.min(mMaxScrollYPix, y); super.scrollTo(x, y); mOnScrollToCallbackHelper.notifyCalled(); } } @Override protected TestDependencyFactory createTestDependencyFactory() { return new TestDependencyFactory() { @Override public AwTestContainerView createAwTestContainerView(AwTestRunnerActivity activity) { return new ScrollTestContainerView(activity); } }; } private static final String TEST_PAGE_COMMON_HEADERS = " " + " "; private static final String TEST_PAGE_COMMON_CONTENT = "
test div
"; private String makeTestPage(String onscrollObserver, String firstFrameObserver, String extraContent) { String content = TEST_PAGE_COMMON_CONTENT + extraContent; if (onscrollObserver != null) { content += ""; } if (firstFrameObserver != null) { content += ""; } return CommonResources.makeHtmlPageFrom(TEST_PAGE_COMMON_HEADERS, content); } private void scrollToOnMainSync(final View view, final int xPix, final int yPix) { getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { view.scrollTo(xPix, yPix); } }); } private void setMaxScrollOnMainSync(final ScrollTestContainerView testContainerView, final int maxScrollXPix, final int maxScrollYPix) { getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { testContainerView.setMaxScrollX(maxScrollXPix); testContainerView.setMaxScrollY(maxScrollYPix); } }); } private boolean checkScrollOnMainSync(final ScrollTestContainerView testContainerView, final int scrollXPix, final int scrollYPix) { final AtomicBoolean equal = new AtomicBoolean(false); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { equal.set((scrollXPix == testContainerView.getScrollX()) && (scrollYPix == testContainerView.getScrollY())); } }); return equal.get(); } private void assertScrollOnMainSync(final ScrollTestContainerView testContainerView, final int scrollXPix, final int scrollYPix) { getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { assertEquals(scrollXPix, testContainerView.getScrollX()); assertEquals(scrollYPix, testContainerView.getScrollY()); } }); } private void assertScrollInJs(final AwContents awContents, final TestAwContentsClient contentsClient, final int xCss, final int yCss) throws Exception { assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { @Override public boolean isSatisfied() { try { String x = executeJavaScriptAndWaitForResult(awContents, contentsClient, "window.scrollX"); String y = executeJavaScriptAndWaitForResult(awContents, contentsClient, "window.scrollY"); return (Integer.toString(xCss).equals(x) && Integer.toString(yCss).equals(y)); } catch (Throwable t) { t.printStackTrace(); fail("Failed to get window.scroll(X/Y): " + t.toString()); return false; } } }, WAIT_TIMEOUT_SECONDS * 1000, CHECK_INTERVAL)); } private void assertScrolledToBottomInJs(final AwContents awContents, final TestAwContentsClient contentsClient) throws Exception { final String isBottomScript = "window.scrollY == " + "(window.document.documentElement.scrollHeight - window.innerHeight)"; assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { @Override public boolean isSatisfied() { try { String r = executeJavaScriptAndWaitForResult(awContents, contentsClient, isBottomScript); return r.equals("true"); } catch (Throwable t) { t.printStackTrace(); fail("Failed to get window.scroll(X/Y): " + t.toString()); return false; } } }, WAIT_TIMEOUT_SECONDS * 1000, CHECK_INTERVAL)); } private void loadTestPageAndWaitForFirstFrame(final ScrollTestContainerView testContainerView, final TestAwContentsClient contentsClient, final String onscrollObserverName, final String extraContent) throws Exception { final JavascriptEventObserver firstFrameObserver = new JavascriptEventObserver(); final String firstFrameObserverName = "firstFrameObserver"; enableJavaScriptOnUiThread(testContainerView.getAwContents()); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { firstFrameObserver.register(testContainerView.getContentViewCore(), firstFrameObserverName); } }); loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(), makeTestPage(onscrollObserverName, firstFrameObserverName, extraContent), "text/html", false); // We wait for "a couple" of frames for the active tree in CC to stabilize and for pending // tree activations to stop clobbering the root scroll layer's scroll offset. This wait // doesn't strictly guarantee that but there isn't a good alternative and this seems to // work fine. firstFrameObserver.waitForEvent(WAIT_TIMEOUT_SECONDS * 1000); } @SmallTest @Feature({"AndroidWebView"}) public void testUiScrollReflectedInJs() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final double deviceDIPScale = DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); final int targetScrollXCss = 233; final int targetScrollYCss = 322; final int targetScrollXPix = (int) Math.ceil(targetScrollXCss * deviceDIPScale); final int targetScrollYPix = (int) Math.ceil(targetScrollYCss * deviceDIPScale); final JavascriptEventObserver onscrollObserver = new JavascriptEventObserver(); Log.w("AndroidScrollIntegrationTest", String.format("scroll in Js (%d, %d) -> (%d, %d)", targetScrollXCss, targetScrollYCss, targetScrollXPix, targetScrollYPix)); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { onscrollObserver.register(testContainerView.getContentViewCore(), "onscrollObserver"); } }); loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, "onscrollObserver", ""); scrollToOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix); onscrollObserver.waitForEvent(SCROLL_OFFSET_PROPAGATION_TIMEOUT_MS); assertScrollInJs(testContainerView.getAwContents(), contentsClient, targetScrollXCss, targetScrollYCss); } @SmallTest @Feature({"AndroidWebView"}) public void testJsScrollReflectedInUi() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final double deviceDIPScale = DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); final int targetScrollXCss = 132; final int targetScrollYCss = 243; final int targetScrollXPix = (int) Math.floor(targetScrollXCss * deviceDIPScale); final int targetScrollYPix = (int) Math.floor(targetScrollYCss * deviceDIPScale); loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(), makeTestPage(null, null, ""), "text/html", false); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient, String.format("window.scrollTo(%d, %d);", targetScrollXCss, targetScrollYCss)); onScrollToCallbackHelper.waitForCallback(scrollToCallCount); assertScrollOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix); } @SmallTest @Feature({"AndroidWebView"}) public void testJsScrollCanBeAlteredByUi() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final double deviceDIPScale = DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); final int targetScrollXCss = 132; final int targetScrollYCss = 243; final int targetScrollXPix = (int) Math.floor(targetScrollXCss * deviceDIPScale); final int targetScrollYPix = (int) Math.floor(targetScrollYCss * deviceDIPScale); final int maxScrollXCss = 101; final int maxScrollYCss = 201; final int maxScrollXPix = (int) Math.floor(maxScrollXCss * deviceDIPScale); final int maxScrollYPix = (int) Math.floor(maxScrollYCss * deviceDIPScale); loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(), makeTestPage(null, null, ""), "text/html", false); setMaxScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient, "window.scrollTo(" + targetScrollXCss + "," + targetScrollYCss + ")"); onScrollToCallbackHelper.waitForCallback(scrollToCallCount); assertScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix); } @SmallTest @Feature({"AndroidWebView"}) public void testTouchScrollCanBeAlteredByUi() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final int dragSteps = 10; final int dragStepSize = 24; // Watch out when modifying - if the y or x delta aren't big enough vertical or horizontal // scroll snapping will kick in. final int targetScrollXPix = dragStepSize * dragSteps; final int targetScrollYPix = dragStepSize * dragSteps; final double deviceDIPScale = DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); final int maxScrollXPix = 101; final int maxScrollYPix = 211; // Make sure we can't hit these values simply as a result of scrolling. assert (maxScrollXPix % dragStepSize) != 0; assert (maxScrollYPix % dragStepSize) != 0; final int maxScrollXCss = (int) Math.floor(maxScrollXPix / deviceDIPScale); final int maxScrollYCss = (int) Math.floor(maxScrollYPix / deviceDIPScale); setMaxScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix); loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); AwTestTouchUtils.dragCompleteView(testContainerView, 0, -targetScrollXPix, // these need to be negative as we're scrolling down. 0, -targetScrollYPix, dragSteps, null /* completionLatch */); for (int i = 1; i <= dragSteps; ++i) { onScrollToCallbackHelper.waitForCallback(scrollToCallCount, i); if (checkScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix)) break; } assertScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix); assertScrollInJs(testContainerView.getAwContents(), contentsClient, maxScrollXCss, maxScrollYCss); } @SmallTest @Feature({"AndroidWebView"}) public void testNoSpuriousOverScrolls() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final int dragSteps = 1; final int targetScrollYPix = 24; setMaxScrollOnMainSync(testContainerView, 0, 0); loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); CountDownLatch scrollingCompleteLatch = new CountDownLatch(1); AwTestTouchUtils.dragCompleteView(testContainerView, 0, 0, // these need to be negative as we're scrolling down. 0, -targetScrollYPix, dragSteps, scrollingCompleteLatch); try { scrollingCompleteLatch.await(); } catch (InterruptedException ex) { // ignore } assertEquals(scrollToCallCount + 1, onScrollToCallbackHelper.getCallCount()); } @SmallTest @Feature({"AndroidWebView"}) public void testOverScrollX() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); final OverScrollByCallbackHelper overScrollByCallbackHelper = testContainerView.getOverScrollByCallbackHelper(); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final int overScrollDeltaX = 30; final int oneStep = 1; loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); // Scroll separately in different dimensions because of vertical/horizontal scroll // snap. final int overScrollCallCount = overScrollByCallbackHelper.getCallCount(); AwTestTouchUtils.dragCompleteView(testContainerView, 0, overScrollDeltaX, 0, 0, oneStep, null /* completionLatch */); overScrollByCallbackHelper.waitForCallback(overScrollCallCount); // Unfortunately the gesture detector seems to 'eat' some number of pixels. For now // checking that the value is < 0 (overscroll is reported as negative values) will have to // do. assertTrue(0 > overScrollByCallbackHelper.getDeltaX()); assertEquals(0, overScrollByCallbackHelper.getDeltaY()); assertScrollOnMainSync(testContainerView, 0, 0); } @SmallTest @Feature({"AndroidWebView"}) public void testOverScrollY() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); final OverScrollByCallbackHelper overScrollByCallbackHelper = testContainerView.getOverScrollByCallbackHelper(); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final int overScrollDeltaY = 30; final int oneStep = 1; loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); int overScrollCallCount = overScrollByCallbackHelper.getCallCount(); AwTestTouchUtils.dragCompleteView(testContainerView, 0, 0, 0, overScrollDeltaY, oneStep, null /* completionLatch */); overScrollByCallbackHelper.waitForCallback(overScrollCallCount); assertEquals(0, overScrollByCallbackHelper.getDeltaX()); assertTrue(0 > overScrollByCallbackHelper.getDeltaY()); assertScrollOnMainSync(testContainerView, 0, 0); } @SmallTest @Feature({"AndroidWebView"}) public void testScrollToBottomAtPageScaleX0dot5() throws Throwable { // The idea behind this test is to check that scrolling to the bottom on ther renderer side // results in the view also reporting as being scrolled to the bottom. final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final int targetScrollXCss = 1000; final int targetScrollYCss = 10000; final String pageHeaders = " " + " "; loadDataSync(testContainerView.getAwContents(), contentsClient.getOnPageFinishedHelper(), CommonResources.makeHtmlPageFrom(pageHeaders, TEST_PAGE_COMMON_CONTENT), "text/html", false); final double deviceDIPScale = DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient, "window.scrollTo(" + targetScrollXCss + "," + targetScrollYCss + ")"); onScrollToCallbackHelper.waitForCallback(scrollToCallCount); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { AwContents awContents = testContainerView.getAwContents(); int maxHorizontal = awContents.computeHorizontalScrollRange() - testContainerView.getWidth(); int maxVertical = awContents.computeVerticalScrollRange() - testContainerView.getHeight(); // Due to rounding going from CSS -> physical pixels it is possible that more than // one physical pixels corespond to one CSS pixel, which is why we can't do a // simple equality test here. assertTrue(maxHorizontal - awContents.computeHorizontalScrollOffset() < 3); assertTrue(maxVertical - awContents.computeVerticalScrollOffset() < 3); } }); scrollToCallCount = onScrollToCallbackHelper.getCallCount(); executeJavaScriptAndWaitForResult(testContainerView.getAwContents(), contentsClient, "window.scrollTo(0, 0)"); onScrollToCallbackHelper.waitForCallback(scrollToCallCount); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { AwContents awContents = testContainerView.getAwContents(); int maxHorizontal = awContents.computeHorizontalScrollRange() - testContainerView.getWidth(); int maxVertical = awContents.computeVerticalScrollRange() - testContainerView.getHeight(); testContainerView.scrollTo(maxHorizontal, maxVertical); } }); assertScrolledToBottomInJs(testContainerView.getAwContents(), contentsClient); } @SmallTest @Feature({"AndroidWebView"}) public void testFlingScroll() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); assertScrollOnMainSync(testContainerView, 0, 0); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { testContainerView.getAwContents().flingScroll(1000, 1000); } }); onScrollToCallbackHelper.waitForCallback(scrollToCallCount); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { assertTrue(testContainerView.getScrollX() > 0); assertTrue(testContainerView.getScrollY() > 0); } }); } @SmallTest @Feature({"AndroidWebView"}) public void testPageDown() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); assertScrollOnMainSync(testContainerView, 0, 0); final int maxScrollYPix = runTestOnUiThreadAndGetResult(new Callable() { @Override public Integer call() { return (testContainerView.getAwContents().computeVerticalScrollRange() - testContainerView.getHeight()); } }); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { testContainerView.getAwContents().pageDown(true); } }); // Wait for the animation to hit the bottom of the page. for (int i = 1; ; ++i) { onScrollToCallbackHelper.waitForCallback(scrollToCallCount, i); if (checkScrollOnMainSync(testContainerView, 0, maxScrollYPix)) break; } } @SmallTest @Feature({"AndroidWebView"}) public void testPageUp() throws Throwable { final TestAwContentsClient contentsClient = new TestAwContentsClient(); final ScrollTestContainerView testContainerView = (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient); enableJavaScriptOnUiThread(testContainerView.getAwContents()); final double deviceDIPScale = DeviceDisplayInfo.create(testContainerView.getContext()).getDIPScale(); final int targetScrollYCss = 243; final int targetScrollYPix = (int) Math.ceil(targetScrollYCss * deviceDIPScale); loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); assertScrollOnMainSync(testContainerView, 0, 0); scrollToOnMainSync(testContainerView, 0, targetScrollYPix); final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper(); final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { testContainerView.getAwContents().pageUp(true); } }); // Wait for the animation to hit the bottom of the page. for (int i = 1; ; ++i) { onScrollToCallbackHelper.waitForCallback(scrollToCallCount, i); if (checkScrollOnMainSync(testContainerView, 0, 0)) break; } } }