@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 = 40;

    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 testJsScrollFromBody() 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 String scrollFromBodyScript =
        "<script> "
            + "  window.scrollTo("
            + targetScrollXCss
            + ", "
            + targetScrollYCss
            + "); "
            + "</script> ";

    final CallbackHelper onScrollToCallbackHelper = testContainerView.getOnScrollToCallbackHelper();
    final int scrollToCallCount = onScrollToCallbackHelper.getCallCount();
    loadDataAsync(
        testContainerView.getAwContents(),
        makeTestPage(null, null, scrollFromBodyScript),
        "text/html",
        false);
    onScrollToCallbackHelper.waitForCallback(scrollToCallCount);

    assertScrollOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix);
  }
  @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();

    getInstrumentation()
        .runOnMainSync(
            new Runnable() {
              @Override
              public void run() {
                onscrollObserver.register(
                    testContainerView.getContentViewCore(), "onscrollObserver");
              }
            });

    loadTestPageAndWaitForFirstFrame(testContainerView, contentsClient, "onscrollObserver", "");

    scrollToOnMainSync(testContainerView, targetScrollXPix, targetScrollYPix);

    onscrollObserver.waitForEvent(WAIT_TIMEOUT_MS);
    assertScrollInJs(
        testContainerView.getAwContents(), contentsClient, targetScrollXCss, targetScrollYCss);
  }
  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_MS);
  }
  @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 testTouchScrollingConsumesScrollByGesture() throws Throwable {
    final TestAwContentsClient contentsClient = new TestAwContentsClient();
    final ScrollTestContainerView testContainerView =
        (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
    final TestGestureStateListener testGestureStateListener = new TestGestureStateListener();
    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>");

    getInstrumentation()
        .runOnMainSync(
            new Runnable() {
              @Override
              public void run() {
                testContainerView
                    .getContentViewCore()
                    .addGestureStateListener(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,
        null /* completionLatch */);
    onScrollUpdateGestureConsumedHelper.waitForCallback(callCount);
  }
  @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<Integer>() {
              @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 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 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;
    }
  }
  @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 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 testPinchZoomUpdatesScrollRangeSynchronously() throws Throwable {
    final TestAwContentsClient contentsClient = new TestAwContentsClient();
    final ScrollTestContainerView testContainerView =
        (ScrollTestContainerView) createAwTestContainerViewOnMainSync(contentsClient);
    final OverScrollByCallbackHelper overScrollByCallbackHelper =
        testContainerView.getOverScrollByCallbackHelper();
    final AwContents awContents = testContainerView.getAwContents();
    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<Float>();
    final AtomicReference<Float> atomicNewScale = new AtomicReference<Float>();
    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();
    getInstrumentation()
        .runOnMainSync(
            new Runnable() {
              @Override
              public void run() {
                canZoomIn.set(awContents.canZoomIn());

                int oldScrollRange =
                    awContents.computeVerticalScrollRange() - testContainerView.getHeight();
                float oldScale = awContents.getScale();
                atomicOldContentHeightApproximation.set(
                    (int) Math.ceil(awContents.computeVerticalScrollRange() / oldScale));

                awContents.zoomIn();

                int newScrollRange =
                    awContents.computeVerticalScrollRange() - testContainerView.getHeight();
                float newScale = awContents.getScale();
                atomicNewContentHeightApproximation.set(
                    (int) Math.ceil(awContents.computeVerticalScrollRange() / newScale));

                atomicOldScale.set(oldScale);
                atomicNewScale.set(newScale);
                atomicOldScrollRange.set(oldScrollRange);
                atomicNewScrollRange.set(newScrollRange);
                atomicContentHeight.set(awContents.getContentHeightCss());
              }
            });
    assertTrue(canZoomIn.get());
    assertTrue(
        String.format(
            Locale.ENGLISH,
            "Scale range should increase after zoom (%f) > (%f)",
            atomicNewScale.get(),
            atomicOldScale.get()),
        atomicNewScale.get() > atomicOldScale.get());
    assertTrue(
        String.format(
            Locale.ENGLISH,
            "Scroll range should increase after zoom (%d) > (%d)",
            atomicNewScrollRange.get(),
            atomicOldScrollRange.get()),
        atomicNewScrollRange.get() > atomicOldScrollRange.get());
    assertEquals(atomicContentHeight.get(), atomicOldContentHeightApproximation.get());
    assertEquals(atomicContentHeight.get(), atomicNewContentHeightApproximation.get());
  }
  @SmallTest
  @Feature({"AndroidWebView"})
  public void testFlingScrollOnPopup() throws Throwable {
    final TestAwContentsClient parentContentsClient = new TestAwContentsClient();
    final ScrollTestContainerView parentContainerView =
        (ScrollTestContainerView) createAwTestContainerViewOnMainSync(parentContentsClient);
    final AwContents parentContents = parentContainerView.getAwContents();
    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");

    triggerPopup(
        parentContents,
        parentContentsClient,
        mWebServer,
        parentPageHtml,
        popupPageHtml,
        popupPath,
        "tryOpenWindow()");
    final PopupInfo popupInfo = connectPendingPopup(parentContents);
    assertEquals("Popup Window", getTitleOnUiThread(popupInfo.popupContents));

    final ScrollTestContainerView testContainerView =
        (ScrollTestContainerView) popupInfo.popupContainerView;
    enableJavaScriptOnUiThread(testContainerView.getAwContents());
    loadTestPageAndWaitForFirstFrame(testContainerView, popupInfo.popupContentsClient, 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);
              }
            });
  }