chromium/third_party/blink/web_tests/fast/scroll-snap/snap-scrolls-visual-viewport.html

<!DOCTYPE html>
<script src="../../resources/gesture-util.js"></script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<style>
html {
  scroll-snap-type: both proximity;
}
body {
  overflow: scroll;
  height: 300px;
  width: 300px;
  margin: 0px;
  padding: 0px;
}
#container {
  margin: 0px;
  padding: 0px;
  width: 600px;
  height: 2000px;
}
#area {
  position: relative;
  left: 100px;
  top: 700px;
  width: 200px;
  height: 200px;
  scroll-snap-align: start;
}

</style>

<div id="container">
  <div id="area"></div>
</div>

<script>
const SOURCE_TYPE = navigator.platform.indexOf('Mac') == 0
                  ? GestureSourceType.MOUSE_INPUT
                  : GestureSourceType.TOUCH_INPUT

async function set_and_scroll_and_snap(
    initial_position,
    scroll_delta,
    scroll_direction,
    layout_viewport_y,
    visual_viewport_y) {
  const scale_factor = 2;
  internals.setPageScaleFactor(scale_factor);
  window.scrollTo(0, initial_position);
  internals.setVisualViewportOffset(0, 0);
  assert_equals(window.visualViewport.scale, 2);
  assert_equals(window.scrollY, initial_position);
  assert_equals(window.visualViewport.offsetTop, 0);

  const x = 200;
  const y = 200;
  const scrollPromise = waitForScrollEvent(document);
  await smoothScroll(scale_factor * scroll_delta,
                     x,
                     y,
                     SOURCE_TYPE,
                     scroll_direction,
                     SPEED_INSTANT);
  await scrollPromise;
  await waitForAnimationEndTimeBased(() => {
    return window.scrollY + window.visualViewport.offsetTop;
  });

  assert_approx_equals(window.scrollY, layout_viewport_y, 1);
  assert_approx_equals(window.visualViewport.offsetTop, visual_viewport_y, 1);
}

promise_test(t => {
  // The starting scroll position is 1000.
  // We scroll upwards 400 to land on 600.
  // As the visual_viewport's position is 0 and has room of 300 to scroll
  // downwards, the layout_viewport can stay at 600 and only the visual_viewport
  // needs to scroll to 100.
  return set_and_scroll_and_snap(1000, 400, 'up', 600, 100);
}, "Snap scrolls visual viewport when it's scrollable.");

promise_test(t => {
  // The starting scroll position is 1000.
  // We scroll upwards 200 to land on 800.
  // As the visual_viewport's position is 0 and has no room to scroll upwards,
  // the layout_viewport is scrolled to 700 to snap to #area.
  return set_and_scroll_and_snap(1000, 200, 'up', 700, 0);
}, "Snap scrolls layout viewport when visual viewport has reached its scroll extent.");

promise_test(t => {
  // The starting scroll position is 300.
  // We scroll downwards 250 to bring visual_viewport to 250, while still keeping
  // layout_viewport at 300. So the total offset is 550.
  // As the visual_viewport's position is 250 and only has room of 50 to scroll
  // downwards, the layout_viewport needs to scroll another 100 downwards to
  // make the total offset as 700.
  return set_and_scroll_and_snap(300, 250, 'down', 400, 300);
}, "Snap scrolls both layout and visual viewports when visual viewport is " +
   "scrollable but does not has enough room to reach the snap position.");

</script>