| // Copyright 2013 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 static org.chromium.android_webview.test.AwActivityTestRule.WAIT_TIMEOUT_MS; |
| |
| import android.annotation.SuppressLint; |
| import android.content.Context; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.SmallTest; |
| |
| import org.junit.After; |
| 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.AwContentsStatics; |
| import org.chromium.android_webview.AwScrollOffsetManager; |
| import org.chromium.android_webview.test.AwActivityTestRule.PopupInfo; |
| import org.chromium.android_webview.test.util.AwTestTouchUtils; |
| import org.chromium.android_webview.test.util.CommonResources; |
| import org.chromium.android_webview.test.util.GraphicsTestUtils; |
| import org.chromium.android_webview.test.util.JavascriptEventObserver; |
| import org.chromium.base.test.util.CallbackHelper; |
| import org.chromium.base.test.util.DisabledTest; |
| import org.chromium.base.test.util.Feature; |
| import org.chromium.content_public.browser.GestureListenerManager; |
| import org.chromium.content_public.browser.GestureStateListener; |
| import org.chromium.content_public.browser.test.util.TestThreadUtils; |
| import org.chromium.net.test.util.TestWebServer; |
| |
| import java.util.Locale; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** Integration tests for synchronous scrolling. */ |
| @RunWith(Parameterized.class) |
| @UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class) |
| public class AndroidScrollIntegrationTest extends AwParameterizedTest { |
| private static final double EPSILON = 1e-5; |
| |
| @Rule public AwActivityTestRule mActivityTestRule; |
| |
| public AndroidScrollIntegrationTest(AwSettingsMutation param) { |
| mActivityTestRule = |
| new AwActivityTestRule(param.getMutation()) { |
| @Override |
| public TestDependencyFactory createTestDependencyFactory() { |
| return new TestDependencyFactory() { |
| @Override |
| public AwScrollOffsetManager createScrollOffsetManager( |
| AwScrollOffsetManager.Delegate delegate) { |
| return new AwScrollOffsetManager(delegate); |
| } |
| |
| @Override |
| public AwTestContainerView createAwTestContainerView( |
| AwTestRunnerActivity activity, |
| boolean allowHardwareAcceleration) { |
| return new ScrollTestContainerView( |
| activity, allowHardwareAcceleration); |
| } |
| }; |
| } |
| }; |
| } |
| |
| private TestWebServer mWebServer; |
| |
| private static class OverScrollByCallbackHelper extends CallbackHelper { |
| int mDeltaX; |
| int mDeltaY; |
| int mScrollRangeY; |
| |
| public int getDeltaX() { |
| assert getCallCount() > 0; |
| return mDeltaX; |
| } |
| |
| public int getDeltaY() { |
| assert getCallCount() > 0; |
| return mDeltaY; |
| } |
| |
| public int getScrollRangeY() { |
| assert getCallCount() > 0; |
| return mScrollRangeY; |
| } |
| |
| public void notifyCalled(int deltaX, int deltaY, int scrollRangeY) { |
| mDeltaX = deltaX; |
| mDeltaY = deltaY; |
| mScrollRangeY = scrollRangeY; |
| 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, boolean allowHardwareAcceleration) { |
| super(context, allowHardwareAcceleration); |
| } |
| |
| 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, scrollRangeY); |
| 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(); |
| } |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| mWebServer = TestWebServer.start(); |
| } |
| |
| @After |
| public void tearDown() { |
| if (mWebServer != null) { |
| mWebServer.shutdown(); |
| } |
| } |
| |
| private static final String TEST_PAGE_COMMON_HEADERS = |
| """ |
| <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" /> |
| <style type="text/css"> |
| body { |
| margin: 0px; |
| } |
| div { |
| width: 10000px; |
| height: 10000px; |
| background-color: blue; |
| } |
| </style>"""; |
| private static final String TEST_PAGE_COMMON_CONTENT = "<div>test div</div> "; |
| |
| private String makeTestPage( |
| String onscrollObserver, String firstFrameObserver, String extraContent) { |
| String content = TEST_PAGE_COMMON_CONTENT + extraContent; |
| if (onscrollObserver != null) { |
| content += |
| String.format( |
| """ |
| <script> |
| window.onscroll = function(oEvent) { |
| %s.notifyJava(); |
| } |
| </script>""", |
| onscrollObserver); |
| } |
| if (firstFrameObserver != null) { |
| content += |
| String.format( |
| """ |
| <script> |
| window.framesToIgnore = 20; |
| window.onAnimationFrame = function(timestamp) { |
| if (window.framesToIgnore == 0) { |
| %s.notifyJava(); |
| } else { |
| window.framesToIgnore -= 1; |
| window.requestAnimationFrame(window.onAnimationFrame); |
| } |
| }; |
| window.requestAnimationFrame(window.onAnimationFrame); |
| </script>""", |
| firstFrameObserver); |
| } |
| return CommonResources.makeHtmlPageFrom(TEST_PAGE_COMMON_HEADERS, content); |
| } |
| |
| private void scrollToOnMainSync(final AwTestContainerView view, final int xPix, final int yPix) |
| throws Throwable { |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> view.scrollTo(xPix, yPix)); |
| mActivityTestRule.waitForVisualStateCallback(view.getAwContents()); |
| } |
| |
| private void setMaxScrollOnMainSync( |
| final ScrollTestContainerView testContainerView, |
| final int maxScrollXPix, |
| final int maxScrollYPix) { |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| testContainerView.setMaxScrollX(maxScrollXPix); |
| testContainerView.setMaxScrollY(maxScrollYPix); |
| }); |
| } |
| |
| private boolean checkScrollOnMainSync( |
| final ScrollTestContainerView testContainerView, |
| final int scrollXPix, |
| final int scrollYPix) { |
| return TestThreadUtils.runOnUiThreadBlockingNoException( |
| () -> |
| scrollXPix == testContainerView.getScrollX() |
| && scrollYPix == testContainerView.getScrollY()); |
| } |
| |
| private int[] getScrollOnMainSync(final ScrollTestContainerView testContainerView) { |
| final int scroll[] = new int[2]; |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| scroll[0] = testContainerView.getScrollX(); |
| scroll[1] = testContainerView.getScrollY(); |
| }); |
| return scroll; |
| } |
| |
| private void assertScrollOnMainSync( |
| final ScrollTestContainerView testContainerView, |
| final int scrollXPix, |
| final int scrollYPix) { |
| int scrolled[] = getScrollOnMainSync(testContainerView); |
| int scrolledXPix = scrolled[0]; |
| int scrolledYPix = scrolled[1]; |
| |
| // Actual scrolling is done using this formula: |
| // floor (scroll_offset_dip * max_offset) / max_scroll_offset_dip |
| // where max_offset is calculated using a ceil operation. |
| // This combination of ceiling and flooring can lead to a deviation from the test |
| // calculation, which simply uses the more direct: |
| // floor (scroll_offset_dip * dip_scale) |
| // |
| // While the math used in the functional code is correct (See crbug.com/261239), it can't |
| // be verified down to the pixel in this test which doesn't have all internal values. |
| // In non-rational cases, this can lead to a deviation of up to one pixel when using |
| // the floor directly. To accomodate this scenario, the test allows a -1 px deviation. |
| // |
| // For example, imagine the following valid values: |
| // scroll_offset_dip = 132 |
| // max_offset = 532 |
| // max_scroll_offset_dip = 399 |
| // dip_scale = 1.33125 |
| // |
| // The functional code will return |
| // floor (132 * 532 / 399) = 176 |
| // The test code will return |
| // floor (132 * 1.33125) = 175 |
| // |
| // For more information, see crbug.com/537343 |
| Assert.assertTrue( |
| "Actual and expected x-scroll offsets do not match. Expected " |
| + scrollXPix |
| + ", actual " |
| + scrolledXPix, |
| scrollXPix == scrolledXPix || scrollXPix == scrolledXPix - 1); |
| Assert.assertTrue( |
| "Actual and expected y-scroll offsets do not match. Expected " |
| + scrollYPix |
| + ", actual " |
| + scrolledYPix, |
| scrollYPix == scrolledYPix || scrollYPix == scrolledYPix - 1); |
| } |
| |
| private void assertScrollInJs( |
| final AwContents awContents, |
| final TestAwContentsClient contentsClient, |
| final double xCss, |
| final double yCss) { |
| AwActivityTestRule.pollInstrumentationThread( |
| () -> { |
| String x = |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, contentsClient, "window.scrollX"); |
| String y = |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, contentsClient, "window.scrollY"); |
| |
| double scrollX = Double.parseDouble(x); |
| double scrollY = Double.parseDouble(y); |
| |
| return Math.abs(xCss - scrollX) < EPSILON && Math.abs(yCss - scrollY) < EPSILON; |
| }); |
| } |
| |
| private void assertScrolledToBottomInJs( |
| final AwContents awContents, final TestAwContentsClient contentsClient) { |
| final String isBottomScript = |
| "window.scrollY == " |
| + "(window.document.documentElement.scrollHeight - window.innerHeight)"; |
| AwActivityTestRule.pollInstrumentationThread( |
| () -> { |
| String r = |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| awContents, contentsClient, isBottomScript); |
| return r.equals("true"); |
| }); |
| } |
| |
| 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"; |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> |
| firstFrameObserver.register( |
| testContainerView.getWebContents(), |
| firstFrameObserverName)); |
| mActivityTestRule.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_MS); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testUiScrollReflectedInJs() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| final double deviceDIPScale = |
| GraphicsTestUtils.dipScaleForContext(testContainerView.getContext()); |
| |
| 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(); |
| |
| double expectedScrollXCss = targetScrollXCss; |
| double expectedScrollYCss = targetScrollYCss; |
| expectedScrollXCss = (double) targetScrollXPix / deviceDIPScale; |
| expectedScrollYCss = (double) targetScrollYPix / deviceDIPScale; |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> |
| onscrollObserver.register( |
| testContainerView.getWebContents(), "onscrollObserver")); |
| |
| loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, "onscrollObserver", ""); |
| |
| scrollToOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix); |
| |
| onscrollObserver.waitForEvent(WAIT_TIMEOUT_MS); |
| assertScrollInJs( |
| testContainerView.getAwContents(), |
| contentsClient, |
| expectedScrollXCss, |
| expectedScrollYCss); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @SuppressLint("DefaultLocale") |
| public void testJsScrollReflectedInUi() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| final double deviceDIPScale = |
| GraphicsTestUtils.dipScaleForContext(testContainerView.getContext()); |
| final int targetScrollXCss = 132; |
| final int targetScrollYCss = 243; |
| final int targetScrollXPix = (int) Math.floor(targetScrollXCss * deviceDIPScale); |
| final int targetScrollYPix = (int) Math.floor(targetScrollYCss * deviceDIPScale); |
| |
| mActivityTestRule.loadDataSync( |
| testContainerView.getAwContents(), |
| contentsClient.getOnPageFinishedHelper(), |
| makeTestPage(null, null, ""), |
| "text/html", |
| false); |
| |
| final CallbackHelper onScrollToCallbackHelper = |
| testContainerView.getOnScrollToCallbackHelper(); |
| final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| testContainerView.getAwContents(), |
| contentsClient, |
| String.format("window.scrollTo(%d, %d);", targetScrollXCss, targetScrollYCss)); |
| onScrollToCallbackHelper.waitForCallback(scrollToCallCount); |
| |
| assertScrollOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testJsScrollFromBody() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| final double deviceDIPScale = |
| GraphicsTestUtils.dipScaleForContext(testContainerView.getContext()); |
| 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 String scrollFromBodyScript = |
| "<script> " |
| + " window.scrollTo(" |
| + targetScrollXCss |
| + ", " |
| + targetScrollYCss |
| + "); " |
| + "</script> "; |
| |
| final CallbackHelper onScrollToCallbackHelper = |
| testContainerView.getOnScrollToCallbackHelper(); |
| final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); |
| mActivityTestRule.loadDataAsync( |
| testContainerView.getAwContents(), |
| makeTestPage(null, null, scrollFromBodyScript), |
| "text/html", |
| false); |
| onScrollToCallbackHelper.waitForCallback(scrollToCallCount); |
| |
| assertScrollOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testJsScrollCanBeAlteredByUi() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| final double deviceDIPScale = |
| GraphicsTestUtils.dipScaleForContext(testContainerView.getContext()); |
| 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); |
| |
| mActivityTestRule.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(); |
| mActivityTestRule.executeJavaScriptAndWaitForResult( |
| testContainerView.getAwContents(), |
| contentsClient, |
| "window.scrollTo(" + targetScrollXCss + "," + targetScrollYCss + ")"); |
| onScrollToCallbackHelper.waitForCallback(scrollToCallCount); |
| |
| assertScrollOnMainSync(testContainerView, maxScrollXPix, maxScrollYPix); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testTouchScrollCanBeAlteredByUi() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.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 = |
| GraphicsTestUtils.dipScaleForContext(testContainerView.getContext()); |
| final int maxScrollXPix = 101; |
| final int maxScrollYPix = 211; |
| // Make sure we can't hit these values simply as a result of scrolling. |
| Assert.assertNotEquals(0, maxScrollXPix % dragStepSize); |
| Assert.assertNotEquals(0, maxScrollYPix % dragStepSize); |
| double maxScrollXCss = maxScrollXPix / deviceDIPScale; |
| double maxScrollYCss = 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); |
| |
| 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); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testOverScrollX() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| final OverScrollByCallbackHelper overScrollByCallbackHelper = |
| testContainerView.getOverScrollByCallbackHelper(); |
| AwActivityTestRule.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); |
| 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. |
| Assert.assertTrue(0 > overScrollByCallbackHelper.getDeltaX()); |
| Assert.assertEquals(0, overScrollByCallbackHelper.getDeltaY()); |
| |
| assertScrollOnMainSync(testContainerView, 0, 0); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testOverScrollY() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| final OverScrollByCallbackHelper overScrollByCallbackHelper = |
| testContainerView.getOverScrollByCallbackHelper(); |
| AwActivityTestRule.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); |
| overScrollByCallbackHelper.waitForCallback(overScrollCallCount); |
| Assert.assertEquals(0, overScrollByCallbackHelper.getDeltaX()); |
| Assert.assertTrue(0 > overScrollByCallbackHelper.getDeltaY()); |
| |
| assertScrollOnMainSync(testContainerView, 0, 0); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @DisabledTest(message = "https://crbug.com/1147838") |
| public void testFlingScroll() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); |
| |
| assertScrollOnMainSync(testContainerView, 0, 0); |
| |
| final CallbackHelper onScrollToCallbackHelper = |
| testContainerView.getOnScrollToCallbackHelper(); |
| final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync(() -> testContainerView.getAwContents().flingScroll(1000, 1000)); |
| |
| onScrollToCallbackHelper.waitForCallback(scrollToCallCount); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| Assert.assertTrue(testContainerView.getScrollX() > 0); |
| Assert.assertTrue(testContainerView.getScrollY() > 0); |
| }); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| @DisabledTest(message = "https://crbug.com/813837") |
| public void testFlingScrollOnPopup() throws Throwable { |
| final TestAwContentsClient parentContentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView parentContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(parentContentsClient); |
| final AwContents parentContents = parentContainerView.getAwContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(parentContents); |
| |
| final String popupPath = "/popup.html"; |
| final String parentPageHtml = |
| CommonResources.makeHtmlPageFrom( |
| "", |
| "<script>" |
| + "function tryOpenWindow() {" |
| + " var newWindow = window.open('" |
| + popupPath |
| + "');" |
| + "}</script> <h1>Parent</h1>"); |
| |
| final String popupPageHtml = |
| CommonResources.makeHtmlPageFrom( |
| "<title>" + "Popup Window" + "</title>", "This is a popup window"); |
| |
| mActivityTestRule.triggerPopup( |
| parentContents, |
| parentContentsClient, |
| mWebServer, |
| parentPageHtml, |
| popupPageHtml, |
| popupPath, |
| "tryOpenWindow()"); |
| final PopupInfo popupInfo = mActivityTestRule.connectPendingPopup(parentContents); |
| Assert.assertEquals( |
| "Popup Window", mActivityTestRule.getTitleOnUiThread(popupInfo.popupContents)); |
| |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) popupInfo.popupContainerView; |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| loadTestPageAndWaitForFirstFrame( |
| testContainerView, popupInfo.popupContentsClient, null, ""); |
| |
| assertScrollOnMainSync(testContainerView, 0, 0); |
| |
| final CallbackHelper onScrollToCallbackHelper = |
| testContainerView.getOnScrollToCallbackHelper(); |
| final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync(() -> testContainerView.getAwContents().flingScroll(1000, 1000)); |
| |
| onScrollToCallbackHelper.waitForCallback(scrollToCallCount); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| Assert.assertTrue(testContainerView.getScrollX() > 0); |
| Assert.assertTrue(testContainerView.getScrollY() > 0); |
| }); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testPageDown() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); |
| |
| assertScrollOnMainSync(testContainerView, 0, 0); |
| |
| final int maxScrollYPix = |
| TestThreadUtils.runOnUiThreadBlocking( |
| () -> |
| (testContainerView.getAwContents().computeVerticalScrollRange() |
| - testContainerView.getHeight())); |
| |
| final CallbackHelper onScrollToCallbackHelper = |
| testContainerView.getOnScrollToCallbackHelper(); |
| final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync(() -> 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; |
| } |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testPageUp() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| final double deviceDIPScale = |
| GraphicsTestUtils.dipScaleForContext(testContainerView.getContext()); |
| 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(); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync(() -> 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; |
| } |
| } |
| |
| private static class TestGestureStateListener extends GestureStateListener { |
| private CallbackHelper mOnScrollUpdateGestureConsumedHelper = new CallbackHelper(); |
| |
| public CallbackHelper getOnScrollUpdateGestureConsumedHelper() { |
| return mOnScrollUpdateGestureConsumedHelper; |
| } |
| |
| @Override |
| public void onPinchStarted() {} |
| |
| @Override |
| public void onPinchEnded() {} |
| |
| @Override |
| public void onFlingStartGesture( |
| int scrollOffsetY, int scrollExtentY, boolean isDirectionUp) {} |
| |
| @Override |
| public void onScrollUpdateGestureConsumed() { |
| mOnScrollUpdateGestureConsumedHelper.notifyCalled(); |
| } |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testTouchScrollingConsumesScrollByGesture() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| final TestGestureStateListener testGestureStateListener = new TestGestureStateListener(); |
| AwActivityTestRule.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; |
| |
| loadTestPageAndWaitForFirstFrame( |
| testContainerView, |
| contentsClient, |
| null, |
| """ |
| <div> |
| <div style="width:10000px; height: 10000px;"> force scrolling </div> |
| </div>"""); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> |
| GestureListenerManager.fromWebContents( |
| testContainerView.getWebContents()) |
| .addListener(testGestureStateListener)); |
| final CallbackHelper onScrollUpdateGestureConsumedHelper = |
| testGestureStateListener.getOnScrollUpdateGestureConsumedHelper(); |
| |
| final int callCount = onScrollUpdateGestureConsumedHelper.getCallCount(); |
| AwTestTouchUtils.dragCompleteView( |
| testContainerView, |
| 0, |
| -targetScrollXPix, // these need to be negative as we're scrolling down. |
| 0, |
| -targetScrollYPix, |
| dragSteps); |
| onScrollUpdateGestureConsumedHelper.waitForCallback(callCount); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature({"AndroidWebView"}) |
| public void testPinchZoomUpdatesScrollRangeSynchronously() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| final OverScrollByCallbackHelper overScrollByCallbackHelper = |
| testContainerView.getOverScrollByCallbackHelper(); |
| final AwContents awContents = testContainerView.getAwContents(); |
| AwActivityTestRule.enableJavaScriptOnUiThread(awContents); |
| |
| loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); |
| |
| // Containers to execute asserts on the test thread |
| final AtomicBoolean canZoomIn = new AtomicBoolean(false); |
| final AtomicReference<Float> atomicOldScale = new AtomicReference<>(); |
| final AtomicReference<Float> atomicNewScale = new AtomicReference<>(); |
| final AtomicInteger atomicOldScrollRange = new AtomicInteger(); |
| final AtomicInteger atomicNewScrollRange = new AtomicInteger(); |
| final AtomicInteger atomicContentHeight = new AtomicInteger(); |
| final AtomicInteger atomicOldContentHeightApproximation = new AtomicInteger(); |
| final AtomicInteger atomicNewContentHeightApproximation = new AtomicInteger(); |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| canZoomIn.set(awContents.canZoomIn()); |
| |
| int oldScrollRange = |
| awContents.computeVerticalScrollRange() |
| - testContainerView.getHeight(); |
| float oldScale = awContents.getScale(); |
| double oldHeight = |
| Math.ceil(awContents.computeVerticalScrollRange() / oldScale); |
| atomicOldContentHeightApproximation.set((int) oldHeight); |
| |
| awContents.zoomIn(); |
| |
| int newScrollRange = |
| awContents.computeVerticalScrollRange() |
| - testContainerView.getHeight(); |
| float newScale = awContents.getScale(); |
| |
| double newHeight = |
| Math.ceil(awContents.computeVerticalScrollRange() / newScale); |
| atomicNewContentHeightApproximation.set((int) newHeight); |
| |
| atomicOldScale.set(oldScale); |
| atomicNewScale.set(newScale); |
| atomicOldScrollRange.set(oldScrollRange); |
| atomicNewScrollRange.set(newScrollRange); |
| atomicContentHeight.set(awContents.getContentHeightCss()); |
| }); |
| Assert.assertTrue(canZoomIn.get()); |
| Assert.assertTrue( |
| String.format( |
| Locale.ENGLISH, |
| "Scale range should increase after zoom (%f) > (%f)", |
| atomicNewScale.get(), |
| atomicOldScale.get()), |
| atomicNewScale.get() > atomicOldScale.get()); |
| Assert.assertTrue( |
| String.format( |
| Locale.ENGLISH, |
| "Scroll range should increase after zoom (%d) > (%d)", |
| atomicNewScrollRange.get(), |
| atomicOldScrollRange.get()), |
| atomicNewScrollRange.get() > atomicOldScrollRange.get()); |
| Assert.assertTrue( |
| String.format( |
| Locale.ENGLISH, |
| "Old content height should be close (%d) ~= (%d)", |
| atomicContentHeight.get(), |
| atomicOldContentHeightApproximation.get()), |
| Math.abs(atomicContentHeight.get() - atomicOldContentHeightApproximation.get()) |
| <= 1); |
| Assert.assertTrue( |
| String.format( |
| Locale.ENGLISH, |
| "New content height should be close (%d) ~= (%d)", |
| atomicContentHeight.get(), |
| atomicNewContentHeightApproximation.get()), |
| Math.abs(atomicContentHeight.get() - atomicNewContentHeightApproximation.get()) |
| <= 1); |
| } |
| |
| @Test |
| @SmallTest |
| @Feature("AndroidWebView") |
| public void testScrollOffsetAfterCapturePicture() throws Throwable { |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| final int targetScrollYPix = 322; |
| |
| loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); |
| |
| assertScrollOnMainSync(testContainerView, 0, 0); |
| |
| scrollToOnMainSync(testContainerView, 0, targetScrollYPix); |
| |
| final int scrolledYPix = |
| TestThreadUtils.runOnUiThreadBlocking(() -> testContainerView.getScrollY()); |
| |
| Assert.assertTrue(scrolledYPix > 0); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync(() -> testContainerView.getAwContents().capturePicture()); |
| |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> Assert.assertEquals(testContainerView.getScrollY(), scrolledYPix)); |
| } |
| |
| // Regression test for crbug.com/1299753. |
| @Test |
| @SmallTest |
| @Feature("AndroidWebView") |
| public void testCanTouchScrollYWithRecordFullDocument() throws Throwable { |
| InstrumentationRegistry.getInstrumentation() |
| .runOnMainSync( |
| () -> { |
| AwContentsStatics.setRecordFullDocument(true); |
| }); |
| |
| final TestAwContentsClient contentsClient = new TestAwContentsClient(); |
| final ScrollTestContainerView testContainerView = |
| (ScrollTestContainerView) |
| mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient); |
| AwActivityTestRule.enableJavaScriptOnUiThread(testContainerView.getAwContents()); |
| |
| // Load page. |
| loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, null, ""); |
| |
| // Check page loaded at scroll offset 0. |
| { |
| int scroll[] = getScrollOnMainSync(testContainerView); |
| Assert.assertEquals(0, scroll[1]); |
| } |
| |
| // Drag scroll. |
| final CallbackHelper onScrollToCallbackHelper = |
| testContainerView.getOnScrollToCallbackHelper(); |
| final int scrollToCallCount = onScrollToCallbackHelper.getCallCount(); |
| final int dragSteps = 10; |
| final int dragStepSize = 24; |
| final int targetScrollYPix = dragStepSize * dragSteps; |
| AwTestTouchUtils.dragCompleteView(testContainerView, 0, 0, 0, -targetScrollYPix, dragSteps); |
| |
| // Poll until scroll on UI is bigger than 0. |
| AwActivityTestRule.pollInstrumentationThread( |
| () -> { |
| int scroll[] = getScrollOnMainSync(testContainerView); |
| return scroll[1] > 0; |
| }); |
| } |
| } |