summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/css/css-scroll-snap
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/css/css-scroll-snap
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/css/css-scroll-snap')
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/META.yml3
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/capturing-snap-positions.html55
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/crashtests/fractional-covering-area-crash.html25
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.html57
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/inheritance.html39
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/input/keyboard.html182
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/input/mouse-wheel.html70
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.tentative.html158
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/nested-scrollIntoView-snaps.html104
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/no-red-ref.html5
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/no-snap-position.html89
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas-nested.tentative.html108
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html180
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/overscroll-snap.html50
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-computed.html42
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-invalid.html46
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-shorthand.html35
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-valid.html36
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-computed.html42
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-invalid.html44
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-shorthand.html43
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-valid.html44
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-computed.html70
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-invalid.html54
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-shorthand.html35
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-valid.html58
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-computed.html71
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-invalid.html52
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-shorthand.html43
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-valid.html60
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-computed.html26
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-invalid.html21
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-valid.html25
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-computed.html20
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.html19
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-valid.html18
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-computed.html27
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-invalid.html30
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-valid.html29
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/resnap-on-snap-alignment-change.html47
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-margin-visibility-check.html44
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-margin.html90
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.tentative.html88
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-padding-and-margin.html50
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-padding.html49
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-nested-snap-area-layout-changed.tentative.html105
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001-ref.html22
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001.html43
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002-ref.html18
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002.html44
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-003.html35
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-001.html96
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002-nested.tentative.html61
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002.html213
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-change.html90
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-dynamic-crash.html15
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-change.html66
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-on-root-element.html81
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type.html87
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-001-ref.html23
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-001.html23
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-002.html60
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-003.html61
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-001.html23
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-002.html57
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-003.html59
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-004.html54
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-005.html36
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-006.html57
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-001.html21
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-002.html56
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-003.html58
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-001.html21
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-002.html70
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-003.html69
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/scrollTo-scrollBy-snaps.html160
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/selection-target.html49
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/direction-rtl.html51
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000-ref.html81
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html113
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000-ref.html188
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000.html238
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/snap-after-initial-layout-ref.html20
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-horizontal-tb.html52
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-lr.html52
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-rl.html55
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.html55
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-snap-area-while-snapped.html66
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.tentative.html119
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html108
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.html95
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html96
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/focus-element-no-snap.html51
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/move-current-target.html116
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element.html128
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-nested-containers.html109
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/resources/common.js148
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/not-resnap-outside-proximity-threshold.html66
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/remove-current-target.html65
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.html82
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.html91
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-add-scroll-container.html154
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.html128
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-at-user-scroll-end.html111
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-inline-block.html115
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-intended-direction.html48
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-into-covering-area.tentative.html77
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-on-focus.html56
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-1.html79
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-2.tentative.html87
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-transformed-target.html56
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-both.html70
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-both.html80
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.html69
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.html69
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-x-axis.html66
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-y-axis.html66
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/support/common.css44
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/support/common.js131
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-align-001-iframe.html40
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-margin-001-iframe.html38
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-padding-001-iframe.html38
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-snap-001-iframe.html55
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-001.html53
-rw-r--r--testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-002.html67
125 files changed, 8458 insertions, 0 deletions
diff --git a/testing/web-platform/tests/css/css-scroll-snap/META.yml b/testing/web-platform/tests/css/css-scroll-snap/META.yml
new file mode 100644
index 0000000000..df776353a3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/META.yml
@@ -0,0 +1,3 @@
+spec: https://drafts.csswg.org/css-scroll-snap/
+suggested_reviewers:
+ - tabatkins
diff --git a/testing/web-platform/tests/css/css-scroll-snap/capturing-snap-positions.html b/testing/web-platform/tests/css/css-scroll-snap/capturing-snap-positions.html
new file mode 100644
index 0000000000..7bc96b9d33
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/capturing-snap-positions.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>scroll-snap-type on non-scrollers traps snap positions</title>
+<link rel=author title="Tab Atkins-Bittner" href="https://www.xanthir.com/contact/">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#captures-snap-positions">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+.scroller {
+ width: 100px;
+ height: 100px;
+ overflow: auto;
+ scroll-snap-type: block mandatory;
+}
+.item {
+ background: gray;
+ height: 100px;
+ scroll-snap-type: block mandatory;
+}
+.item.snapped {
+ background: green;
+ scroll-snap-align: center;
+}
+.subitem {
+ background: red;
+ height: 100px;
+ scroll-snap-align: center;
+}
+
+</style>
+
+
+<!--
+Boxes with a non-none value for scroll-snap-type
+will capture snap positions from their descendents,
+preventing them from affecting higher-up scrollers,
+even if they are not, themselves, scrollers.
+-->
+
+<div class=scroller>
+ <div class=item></div>
+ <div class=item><div class=subitem></div></div>
+ <div class="item snapped"></div>
+ <div class=item></div>
+</div>
+
+<script>
+
+test(()=>{
+ const el = document.querySelector('.scroller');
+ assert_equals(el.scrollTop, 200);
+}, "The third item should be snapped to by default, not the second's child.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/crashtests/fractional-covering-area-crash.html b/testing/web-platform/tests/css/css-scroll-snap/crashtests/fractional-covering-area-crash.html
new file mode 100644
index 0000000000..6905b0fd39
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/crashtests/fractional-covering-area-crash.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<style>
+ #container {
+ scroll-snap-type: both mandatory;
+ overflow: overlay;
+ position: absolute;
+ height: 33554400px;
+ width: 33554400px;
+ }
+
+ #area {
+ scroll-snap-align: end;
+ position: absolute;
+ left: 33554400px;
+ top: -57971.9px;
+ width: 0px;
+ height: 33554400px;
+ }
+</style>
+<div id="container">
+ <div id="area"></div>
+</div>
+
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.html b/testing/web-platform/tests/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.html
new file mode 100644
index 0000000000..b0cde7fed1
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/ignore-snap-points-orthogonal-to-snap-axis.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<title>Ignore snap points orthogonal to scroll snap axis</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#snap-axis" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: x mandatory;
+}
+
+#y-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 0;
+ background-color: green;
+ /* align only on y-axis */
+ scroll-snap-align: start none;
+}
+
+#x-target {
+ width: 300px;
+ height: 300px;
+ top: 0;
+ left: 100px;
+ background-color: red;
+ scroll-snap-align: none start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="x-target"></div>
+ <div id="y-target"></div>
+</div>
+
+<script>
+test(t => {
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Ignore snap points orthogonal to scroll snap axis");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/inheritance.html b/testing/web-platform/tests/css/css-scroll-snap/inheritance.html
new file mode 100644
index 0000000000..2569cf3d8b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/inheritance.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Inheritance of CSS Scroll Snap properties</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#property-index">
+<meta name="assert" content="Properties inherit or not according to the spec.">
+<meta name="assert" content="Properties have initial values according to the spec.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/inheritance-testcommon.js"></script>
+</head>
+<body>
+<div id="container">
+<div id="target"></div>
+</div>
+<script>
+assert_not_inherited('scroll-margin-block-end', '0px', '10px');
+assert_not_inherited('scroll-margin-block-start', '0px', '10px');
+assert_not_inherited('scroll-margin-bottom', '0px', '10px');
+assert_not_inherited('scroll-margin-inline-end', '0px', '10px');
+assert_not_inherited('scroll-margin-inline-start', '0px', '10px');
+assert_not_inherited('scroll-margin-left', '0px', '10px');
+assert_not_inherited('scroll-margin-right', '0px', '10px');
+assert_not_inherited('scroll-margin-top', '0px', '10px');
+assert_not_inherited('scroll-padding-block-end', 'auto', '10px');
+assert_not_inherited('scroll-padding-block-start', 'auto', '10px');
+assert_not_inherited('scroll-padding-bottom', 'auto', '10px');
+assert_not_inherited('scroll-padding-inline-end', 'auto', '10px');
+assert_not_inherited('scroll-padding-inline-start', 'auto', '10px');
+assert_not_inherited('scroll-padding-left', 'auto', '10px');
+assert_not_inherited('scroll-padding-right', 'auto', '10px');
+assert_not_inherited('scroll-padding-top', 'auto', '10px');
+assert_not_inherited('scroll-snap-align', 'none', 'start end');
+assert_not_inherited('scroll-snap-stop', 'normal', 'always');
+assert_not_inherited('scroll-snap-type', 'none', 'inline');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/input/keyboard.html b/testing/web-platform/tests/css/css-scroll-snap/input/keyboard.html
new file mode 100644
index 0000000000..79d0fc9f36
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/input/keyboard.html
@@ -0,0 +1,182 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<title>Arrow key scroll snapping</title>
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<meta name="flags" content="should">
+<meta name="assert"
+ content="Test passes if keyboard scrolling correctly snaps on a snap
+ container">
+
+<link rel="stylesheet" href="../support/common.css">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/dom/events/scrolling/scroll_support.js"></script>
+<script src="../support/common.js"></script>
+
+
+<div id="scroller" tabindex="0">
+ <div id="space"></div>
+ <div class="snap left top" id="top-left"></div>
+ <div class="snap right top" id="top-right"></div>
+ <div class="snap left bottom" id="bottom-left"></div>
+</div>
+
+<script>
+const scroller = document.getElementById("scroller");
+const topLeft = document.getElementById("top-left");
+const topRight = document.getElementById("top-right");
+
+scrollLeft = () => scroller.scrollLeft;
+scrollTop = () => scroller.scrollTop;
+
+function ScrollCounter(test, eventTarget) {
+ this.count = 0;
+ const scrollListener = () => {
+ this.count++;
+ }
+ eventTarget.addEventListener('scroll', scrollListener);
+ test.add_cleanup(() => {
+ eventTarget.removeEventListener('scroll', scrollListener);
+ });
+}
+
+async function initializeScrollPosition(scroller, x, y) {
+ return new Promise(async (resolve) => {
+ if (scroller.scrollLeft != x || scroller.scrollTop != y) {
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ scroller.scrollTo(x, y);
+ await scrollEndPromise;
+ }
+ resolve();
+ });
+}
+
+promise_test(async t => {
+ await initializeScrollPosition(scroller, 0, 0);
+ assert_equals(scroller.scrollTop, 0, "verify test pre-condition");
+ const scrollCounter = new ScrollCounter(t, scroller);
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowDown");
+ await scrollEndPromise;
+ assert_equals(scroller.scrollTop, 400);
+ // Make sure we don't jump directly to the new snap position.
+ assert_greater_than(scrollCounter.count, 1);
+}, "Snaps to bottom-left after pressing ArrowDown");
+
+promise_test(async t => {
+ await initializeScrollPosition(scroller, 0, 400);
+ assert_equals(scroller.scrollTop, 400, "verify test pre-condition");
+ const scrollCounter = new ScrollCounter(t, scroller);
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowUp");
+ await scrollEndPromise;
+ assert_equals(scroller.scrollTop, 0);
+ // Make sure we don't jump directly to the new snap position.
+ assert_greater_than(scrollCounter.count, 1);
+}, "Snaps to top-left after pressing ArrowUp");
+
+promise_test(async t => {
+ await initializeScrollPosition(scroller, 0, 0);
+ assert_equals(scroller.scrollTop, 0, "verify test pre-condition");
+ const scrollCounter = new ScrollCounter(t, scroller);
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowRight");
+ await scrollEndPromise;
+ assert_equals(scroller.scrollLeft, 400);
+ // Make sure we don't jump directly to the new snap position.
+ assert_greater_than(scrollCounter.count, 1);
+}, "Snaps to top-right after pressing ArrowRight");
+
+promise_test(async t => {
+ await initializeScrollPosition(scroller, 400, 0);
+ assert_equals(scroller.scrollLeft, 400, "verify test pre-condition");
+ const scrollCounter = new ScrollCounter(t, scroller);
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowLeft");
+ await scrollEndPromise;
+ assert_equals(scroller.scrollLeft, 0);
+ // Make sure we don't jump directly to the new snap position.
+ assert_greater_than(scrollCounter.count, 1);
+}, "Snaps to top-left after pressing ArrowLeft");
+
+promise_test(async t => {
+ t.add_cleanup(function() {
+ topLeft.style.width = "";
+ topRight.style.left = "400px";
+ });
+
+ // Make the snap area cover the snapport.
+ topLeft.style.width = "800px";
+ // Make the distance between the previous and the next snap position larger
+ // than snapport.
+ topRight.style.left = "500px";
+ await initializeScrollPosition(scroller, 0, 0);
+ assert_equals(scroller.scrollLeft, 0, "verify test pre-condition");
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowRight");
+ await scrollEndPromise;
+ assert_between_exclusive(scroller.scrollLeft, 0, 500);
+}, "If the original intended offset is valid as making a snap area cover the"
++ "snapport, and there's no other snap offset in between, use the original"
++ "intended offset");
+
+promise_test(async t => {
+ t.add_cleanup(function() {
+ topLeft.style.width = "";
+ topRight.style.left = "400px";
+ topRight.style.scrollSnapStop = "";
+ });
+
+ // Make the snap area cover the snapport.
+ topLeft.style.width = "800px";
+ // Make the next snap offset closer than the original intended offset.
+ topRight.style.left = "20px";
+ topRight.style.scrollSnapStop = "always";
+ await initializeScrollPosition(scroller, 0, 0);
+ assert_equals(scroller.scrollLeft, 0, "verify test pre-condition");
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowRight");
+ await scrollEndPromise;
+ assert_equals(scroller.scrollLeft, 20);
+}, "If the original intended offset is valid as making a snap area cover the "
++ "snapport, but there's a defined snap offset in between, use the defined snap"
++ " offset.");
+
+promise_test(async t => {
+ await initializeScrollPosition(scroller, 400, 0);
+ await keyPress(scroller, "ArrowRight");
+ await waitForScrollStop(scroller);
+ assert_equals(scroller.scrollLeft, 400);
+}, "If there is no valid snap offset on the arrow key's direction other than "
++ "the current offset, and the scroll-snap-type is mandatory, stay at the "
++ "current offset.");
+
+promise_test(async t => {
+ t.add_cleanup(function() {
+ scroller.style.scrollSnapType = "both mandatory";
+ scroller.style.width = "";
+ topLeft.style.width = "";
+ });
+
+ scroller.style.scrollSnapType = "both proximity";
+
+ // This test case works only if the scroll amount of pressing "ArrowRight" is
+ // greater than the scroll snap proximity value of the scroll container.
+ // "100px" width of the scroll container works on major browsers.
+ scroller.style.width = "100px";
+ topLeft.style.width = "80px";
+
+ await initializeScrollPosition(scroller, 400, 0);
+ assert_equals(scroller.scrollLeft, 400, "verify test pre-condition");
+ const scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowRight");
+ await scrollEndPromise;
+ assert_greater_than(scroller.scrollLeft, 400);
+}, "If there is no valid snap offset on the arrow key's direction other than "
++ "the current offset, and the scroll-snap-type is proximity, go to the "
++ "original intended offset");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/input/mouse-wheel.html b/testing/web-platform/tests/css/css-scroll-snap/input/mouse-wheel.html
new file mode 100644
index 0000000000..287e594cab
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/input/mouse-wheel.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<title>Mouse-wheel scroll snapping speed</title>
+<meta name="assert"
+ content="Test passes if mousewheel snaps without pausing">
+<style>
+ #scroller {
+ scroll-snap-type: block mandatory;
+ overflow: scroll;
+ height: 400px;
+ width: 400px
+ }
+ #space {
+ width: 200px;
+ height: 4000px;
+ }
+ .box {
+ scroll-snap-align: start;
+ background: blue;
+ margin-bottom: 10px;
+ width: 100px;
+ height: 100px;
+ }
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="../support/common.js"></script>
+<div id="scroller">
+ <div class="box"></div>
+ <div class="box"></div>
+ <div id="space"></div>
+</div>
+<script>
+promise_test(async t => {
+ const scroller = document.getElementById("scroller");
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollTop, 0, "verify test pre-condition");
+ const scrollTop = () => {
+ return scroller.scrollTop;
+ };
+ const scrollPromise = waitForScrollEvent(scroller);
+ const wheelPromise = waitForWheelEvent(scroller);
+ const actions = new test_driver.Actions()
+ .scroll(50, 50, 0, 50, {origin: scroller, duration: 100});
+ await actions.send();
+ await wheelPromise;
+ await scrollPromise;
+ let scrollEndTime;
+ let snapEndTime;
+ // Detect first pause in scrolling.
+ const scrollStabilizedPromise =
+ waitForAnimationEnd(scrollTop).then((timestamp) => {
+ scrollEndTime = timestamp;
+ });
+ const snapEndPromise =
+ waitForScrollTo(scroller, () => {
+ return scroller.scrollTop;
+ }, 110).then((evt) => {
+ snapEndTime = evt.timestamp;
+ });
+ await Promise.all([scrollStabilizedPromise, snapEndPromise]);
+ assert_equals(scroller.scrollTop, 110,
+ 'Failed to advance to next snap target');
+ assert_true(snapEndTime < scrollEndTime,
+ 'Detected pause in scrolling before reaching snap target');
+}, "Wheel-scroll triggers snap to target position immediately.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.tentative.html
new file mode 100644
index 0000000000..0978f127fa
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/input/snap-area-overflow-boundary-viewport-covering.tentative.html
@@ -0,0 +1,158 @@
+<!DOCTYPE html>
+<link rel="help" href="https://www.w3.org/TR/css-scroll-snap-1/#snap-overflow" />
+<title></title>
+<meta name="assert" content="Test passes if snap is to the nearest edge">
+<style>
+ body {
+ margin: 0px;
+ }
+ #scroller {
+ scroll-snap-type: block mandatory;
+ overflow-y: scroll;
+ height: 400px;
+ width: 400px
+ }
+ #space {
+ width: 200px;
+ height: 4000px;
+ }
+ .box {
+ scroll-snap-align: start;
+ background: #ccccff;
+ margin-bottom: 10px;
+ width: 300px;
+ height: 500px;
+ position: relative;
+ }
+ .header {
+ top: 0;
+ position: absolute;
+ }
+ .footer {
+ bottom: 0;
+ position: absolute;
+ }
+</style>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/dom/events/scrolling/scroll_support.js"></script>
+<script src="../support/common.js"></script>
+
+<div id="scroller" tabindex="0">
+ <div id="target" class="box">
+ <div class="header">Header 1</div>
+ <div class="footer">Footer 1</div>
+ </div>
+ <div id="next" class="box">
+ <div class="header">Header 2</div>
+ <div class="footer">Footer 2</div>
+ </div>
+ <div id="space"></div>
+</div>
+
+<script>
+// If all of the following conditions are met:
+// 1. the snap area is larger than the snapport along the scroll axis, and
+// 2. the distance between the previous and subsequent snap positions along
+// the axis is greater then the snapport size.
+//
+// Then any scroll position in which the snap area covers the snapport is
+// valid snap position. This rule facilitates scrolling around in oversized
+// elements.
+//
+// These test covers edge cases with snap-areas that overflow the snapport.
+// It should be possible to scroll to the end of an oversized snap-area.
+
+const scroller = document.getElementById("scroller");
+const target = document.getElementById("target");
+const next = document.getElementById("next");
+const scrollTop = () => {
+ return scroller.scrollTop;
+};
+const cleanup = () => {
+ target.style.height = '500px';
+};
+
+promise_test(async t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollTop, 0, "verify test pre-condition");
+
+ // Ensure we can freely scroll in an oversized element.
+ let scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowDown");
+ await scrollEndPromise;
+ assert_greater_than(scroller.scrollTop, 0,
+ 'Arrowkey scroll moved scroll position');
+ assert_less_than_equal(scroller.scrollTop, target.clientHeight,
+ 'Scroll within snap-area overflow');
+
+ // Resize the element so it is oversized by less than the line scroll amount.
+ // The next keyboard-triggered scroll should stop at the end of the snap-area.
+ // Otherwise it is not possible to scroll to the last line of the snap-area
+ // via keyboard.
+ const scrollAmount = scroller.scrollTop;
+ target.style.height = `${scroller.clientHeight + 2 * scrollAmount - 1}px`;
+ assert_equals(scroller.scrollTop, scrollAmount, "Verify container remains " +
+ "at the same covering snap offset.");
+ scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowDown");
+ await scrollEndPromise;
+ assert_equals(scroller.scrollTop,
+ target.clientHeight - scroller.clientHeight,
+ 'End boundary of snap-area is valid snap target');
+
+ // Must not get stuck at a snap position. Since already at the end of the
+ // snap area, we should advance to the next.
+ scrollEndPromise = waitForScrollEnd(scroller);
+ await keyPress(scroller, "ArrowDown");
+ await scrollEndPromise;
+ assert_equals(scroller.scrollTop,
+ next.offsetTop,
+ 'Advance to next snap-area');
+
+}, "Keyboard scrolling with vertical snap-area overflow");
+
+promise_test(async t => {
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollTop, 0, "verify test pre-condition");
+
+ // Ensure we can freely scroll in an oversized element.
+ let scrollEndPromise = waitForScrollEnd(scroller);
+ await new test_driver.Actions()
+ .scroll(50, 50, 0, 50, {origin: scroller})
+ .send();
+ await scrollEndPromise;
+ assert_equals(scroller.scrollTop, 50,
+ 'Wheel-scroll moved scroll position');
+
+ // Target position for wheel scroll overshoots the boundary of the snap-area.
+ // Ensure that we stop at the boundary.
+ let scrollAmount =
+ target.clientHeight - scroller.clientHeight - scroller.scrollTop + 1;
+
+ scrollEndPromise = waitForScrollEnd(scroller);
+ await new test_driver.Actions()
+ .scroll(50, 50, 0, scrollAmount, {origin: scroller})
+ .send();
+ await scrollEndPromise;
+ assert_equals(scroller.scrollTop, 100,
+ 'End boundary of snap-area is valid snap target');
+
+ // Must not get stuck at a snap position. Since already at the end of the
+ // snap area, we should advance to the next. scrollAmount must be enough to
+ // advance to next snap position.
+ scrollAmount = next.clientHeight / 2 + 10 /* margin-bottom */;
+ scrollEndPromise = waitForScrollEnd(scroller);
+ await new test_driver.Actions()
+ .scroll(50, 50, 0, scrollAmount, {origin: scroller})
+ .send();
+ await scrollEndPromise;
+ assert_equals(scroller.scrollTop, next.offsetTop,
+ 'Advance to next snap-area');
+
+}, "Mouse-wheel scrolling with vertical snap-area overflow");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/nested-scrollIntoView-snaps.html b/testing/web-platform/tests/css/css-scroll-snap/nested-scrollIntoView-snaps.html
new file mode 100644
index 0000000000..b7a2d6551d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/nested-scrollIntoView-snaps.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<meta name="viewport" content="user-scalable=no">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+}
+:root {
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+}
+.scroller {
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+ padding: 0px;
+}
+#outer {
+ left: 1000px;
+ top: 1000px;
+ width: 600px;
+ height: 600px;
+}
+#out-snap-1 {
+ scroll-snap-align: start;
+ left: 1200px;
+ top: 1200px;
+ width: 10px;
+ height: 10px;
+}
+#out-snap-2 {
+ scroll-snap-align: start;
+ left: 1100px;
+ top: 1100px;
+ width: 10px;
+ height: 10px;
+}
+#inner {
+ left: 1000px;
+ top: 1000px;
+ width: 400px;
+ height: 400px;
+}
+.space {
+ left: 0px;
+ top: 0px;
+ width: 3000px;
+ height: 3000px;
+}
+#target {
+ scroll-snap-align: end;
+ left: 800px;
+ top: 800px;
+ width: 200px;
+ height: 200px;
+}
+</style>
+
+<div class="space"></div>
+<div id="out-snap-1"></div>
+<div id="out-snap-2"></div>
+<div class="scroller" id="outer">
+ <div class="space"></div>
+ <div class="scroller" id="inner">
+ <div class="space"></div>
+ <div id="target"></div>
+ </div>
+</div>
+
+<script>
+var outer = document.getElementById("outer");
+var inner = document.getElementById("inner");
+var target = document.getElementById("target");
+
+test(() => {
+ // Initial layout triggers a scroll snap. Reset position before calling
+ // scrollIntoView.
+ window.scrollTo(0, 0);
+ outer.scrollTo(0, 0);
+ inner.scrollTo(0, 0);
+
+ target.scrollIntoView({inline: "start", block: "start"});
+ // Although the scrollIntoView specified "start" as the alignment, the target
+ // has "end" as its snap-alignment. So the inner scroller finishes with "end"
+ // alignment
+ assert_equals(inner.scrollLeft, 1000 - inner.clientWidth, "ScrollIntoView lands on the target's snap position regardless of the specified alignment.");
+ assert_equals(inner.scrollTop, 1000 - inner.clientHeight, "ScrollIntoView lands on the target's snap position regardless of the specified alignment.");
+
+ // Since there is no snap points defined in the outer scroller, the outer
+ // scroller finishes with "start" alignment, as specified in scrollIntoView.
+ // Note that the "start" alignment aligns the target's top-left corner
+ //(inner.left + inner.clientWidth - target.width, inner.top + inner.clientHeight - target.height)
+ // with the outer scroller's top-left corner.
+ assert_equals(outer.scrollLeft, 800 + inner.clientWidth, "ScrollIntoView ends with the specified alignment if no snap position is specified.");
+ assert_equals(outer.scrollTop, 800 + inner.clientHeight, "ScrollIntoView ends with the specified alignment if no snap position is specified.");
+
+ // Although the scrollIntoView specified "start" as the alignment, the window
+ // has two other elements with snap points. So the window finishes with the
+ // closest snap point.
+ assert_equals(window.scrollX, 1100, "ScrollIntoView lands on the snap position closest to the specified alignment.");
+ assert_equals(window.scrollY, 1100, "ScrollIntoView lands on the snap position closest to the specified alignment.");
+}, "All the scrollers affected by scrollIntoView should land on a snap position if one exists. Otherwise, land according to the specified alignment");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/no-red-ref.html b/testing/web-platform/tests/css/css-scroll-snap/no-red-ref.html
new file mode 100644
index 0000000000..061454fb67
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/no-red-ref.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<html>
+<title>CSS Reference: No Red</title>
+
+<p>Test passes if there is no red.
diff --git a/testing/web-platform/tests/css/css-scroll-snap/no-snap-position.html b/testing/web-platform/tests/css/css-scroll-snap/no-snap-position.html
new file mode 100644
index 0000000000..8f1f44af59
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/no-snap-position.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#valdef-scroll-snap-type-mandatory" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./support/common.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+}
+.child {
+ width: 300px;
+ height: 300px;
+ background-color: blue;
+}
+</style>
+
+<div id="scroller">
+ <div class="child" style="top: 0px; left: 0px;"></div>
+ <div class="child" style="top: 1000px; left: 1000px;"></div>
+ <div style="width: 2000px; height: 2000px;"></div>
+</div>
+
+<script>
+test(() => {
+ scroller.style.scrollSnapType = "both mandatory";
+
+ // Scroll to where the first child is in view.
+ scroller.scrollTo(100, 100);
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 100);
+
+ // Scroll to where the second child is in view.
+ scroller.scrollTo(900, 900);
+ assert_equals(scroller.scrollLeft, 900);
+ assert_equals(scroller.scrollTop, 900);
+}, "No snapping occurs if there is no valid snap position");
+
+test(() => {
+ scroller.style.scrollSnapType = "x mandatory";
+
+ for (const target of document.querySelectorAll(".child")) {
+ target.scrollSnapAlign = "start none";
+ }
+
+ // Scroll to where the first child is in view.
+ scroller.scrollTo(100, 100);
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 100);
+
+ // Scroll to where the second child is in view.
+ scroller.scrollTo(900, 900);
+ assert_equals(scroller.scrollLeft, 900);
+ assert_equals(scroller.scrollTop, 900);
+}, "No snapping occurs if there is no valid snap position matches scroll-snap-type");
+
+promise_test(async t => {
+ // Start with valid snap positions.
+ scroller.style.scrollSnapType = "y mandatory";
+ document.querySelectorAll('.child').forEach(el => {
+ el.style.scrollSnapAlign = 'start';
+ t.add_cleanup(() => {
+ el.style.scrollSnapAlign = '';
+ });
+ });
+ scroller.scrollTo(100, 100);
+ await waitForNextFrame();
+ const scrollPosition = scroller.scrollTop;
+ // Elements no longer snap along the y-axis.
+ document.querySelectorAll('.child').forEach(el => {
+ el.style.scrollSnapAlign = 'none start';
+ // Bump the position to verify that we don't stay pinned to the same element
+ // after layout update.
+ el.style.top = `${parseInt(el.style.top) + 100}px`;
+ });
+ await waitForNextFrame();
+ assert_equals(scroller.scrollTop, scrollPosition);
+ scroller.scrollTo(900, 900);
+ assert_equals(scroller.scrollLeft, 900);
+ assert_equals(scroller.scrollTop, 900);
+
+}, "No snapping occurs when last remaining valid snap point is no longer " +
+ "valid.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas-nested.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas-nested.tentative.html
new file mode 100644
index 0000000000..046f3c88ed
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas-nested.tentative.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+}
+.scroller-x {
+ overflow: scroll;
+ scroll-snap-type: x mandatory;
+ width: 500px;
+ height: 500px;
+}
+.scroller-y {
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+ width: 500px;
+ height: 500px;
+}
+.space {
+ width: 4000px;
+ height: 4000px;
+}
+.target {
+ scroll-snap-align: start;
+ height: 400px;
+ width: 400px;
+}
+.large-x {
+ width: 3000px;
+ background-color: yellow;
+}
+.large-y {
+ height: 2000px;
+ background-color: yellow;
+}
+.small {
+ height: 200px;
+ width: 200px;
+ background-color: black;
+}
+</style>
+<div class="scroller-x" id="one-target">
+ <div class="space"></div>
+ <div class="large-x target" id="single" style="left: 200px;"></div>
+</div>
+
+<div class="scroller-x" id="x">
+ <div class="space"></div>
+ <div style="left: 200px;">
+ <div class="target large-x"></div>
+ <div class="target small" style="left: 200px"></div>
+ <div class="target small" style="left: 600px"></div>
+ <div class="target small" style="left: 1200px"></div>
+ </div>
+</div>
+
+<div class="scroller-y" id="y">
+ <div class="space"></div>
+ <div style="top: 200px;">
+ <div class="target large-y"></div>
+ <div class="target small" style="top: 200px"></div>
+ <div class="target small" style="top: 600px"></div>
+ <div class="target small" style="top: 1200px"></div>
+ <div class="target large-y" style="top: 2000px"></div>
+ </div>
+</div>
+
+<div class="scroller-x" id="two-axes" style="scroll-snap-type: both mandatory">
+ <div class="space"></div>
+ <div class="target large-x" style="top: 200px"></div>
+</div>
+
+<div class="scroller-x" id="overlapping-overflow" style="scroll-snap-type: both mandatory">
+ <div class="space"></div>
+ <div style="left: 200px; top: 200px;">
+ <div class="target small"></div>
+ <div class="target small"></div>
+ <div class="target small"></div>
+ <div class="target large-y large-x"></div>
+ <div class="target small"></div>
+ <div class="target small"></div>
+ <div class="target small"></div>
+ </div>
+</div>
+
+<script>
+var one_target_scroller = document.getElementById("one-target");
+var scroller_x = document.getElementById("x");
+var scroller_y = document.getElementById("y");
+var two_axes_scroller = document.getElementById("two-axes");
+var overlapping_scroller = document.getElementById("overlapping-overflow");
+
+test(() => {
+ scroller_x.scrollTo(950, 0);
+ assert_equals(scroller_x.scrollLeft, 1000);
+ assert_equals(scroller_x.scrollTop, 0);
+}, "Snap within a snap area which covers snapport on x selects a valid snap " +
+ "position that avoids the overlapping areas at 800-1000 and 1400-1600.");
+
+test(() => {
+ scroller_y.scrollTo(0, 950);
+ assert_equals(scroller_y.scrollLeft, 0);
+ assert_equals(scroller_y.scrollTop, 1000);
+}, "Snap within a snap area which covers snapport on y selects a valid snap " +
+ "position that avoids the overlapping areas at 800-1000 and 1400-1600.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html b/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html
new file mode 100644
index 0000000000..1e72710811
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/overflowing-snap-areas.html
@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+}
+.scroller-x {
+ overflow: scroll;
+ scroll-snap-type: x mandatory;
+ width: 500px;
+ height: 500px;
+}
+.scroller-y {
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+ width: 500px;
+ height: 500px;
+}
+.space {
+ width: 4000px;
+ height: 4000px;
+}
+.target {
+ scroll-snap-align: start;
+ height: 400px;
+ width: 400px;
+}
+.large-x {
+ width: 3000px;
+ background-color: yellow;
+}
+.large-y {
+ height: 2000px;
+ background-color: yellow;
+}
+.small {
+ height: 200px;
+ width: 200px;
+ background-color: black;
+}
+</style>
+<div class="scroller-x" id="one-target">
+ <div class="space"></div>
+ <div class="large-x target" id="single" style="left: 200px;"></div>
+</div>
+
+<div class="scroller-x" id="x">
+ <div class="space"></div>
+ <div style="left: 200px;">
+ <div class="target large-x"></div>
+ <div class="target small" style="left: 200px"></div>
+ <div class="target small" style="left: 600px"></div>
+ <div class="target small" style="left: 1200px"></div>
+ </div>
+</div>
+
+<div class="scroller-y" id="y">
+ <div class="space"></div>
+ <div style="top: 200px;">
+ <div class="target large-y"></div>
+ <div class="target small" style="top: 200px"></div>
+ <div class="target small" style="top: 600px"></div>
+ <div class="target small" style="top: 1200px"></div>
+ <div class="target large-y" style="top: 2000px"></div>
+ </div>
+</div>
+
+<div class="scroller-x" id="two-axes" style="scroll-snap-type: both mandatory">
+ <div class="space"></div>
+ <div class="target large-x" style="top: 200px"></div>
+</div>
+
+<div class="scroller-x" id="overlapping-overflow" style="scroll-snap-type: both mandatory">
+ <div class="space"></div>
+ <div style="left: 200px; top: 200px;">
+ <div class="target small"></div>
+ <div class="target small"></div>
+ <div class="target small"></div>
+ <div class="target large-y large-x"></div>
+ <div class="target small"></div>
+ <div class="target small"></div>
+ <div class="target small"></div>
+ </div>
+</div>
+
+<script>
+var one_target_scroller = document.getElementById("one-target");
+var scroller_x = document.getElementById("x");
+var scroller_y = document.getElementById("y");
+var two_axes_scroller = document.getElementById("two-axes");
+var overlapping_scroller = document.getElementById("overlapping-overflow");
+
+test(() => {
+ one_target_scroller.scrollTo(10, 0);
+ assert_equals(one_target_scroller.scrollLeft, 200);
+ assert_equals(one_target_scroller.scrollTop, 0);
+}, "Snaps to the snap position if the snap area doesn't cover the snapport on x.");
+
+test(() => {
+ var right_align = 3200 - one_target_scroller.clientWidth;
+ one_target_scroller.scrollTo(right_align, 0);
+ assert_equals(one_target_scroller.scrollLeft, right_align);
+ assert_equals(one_target_scroller.scrollTop, 0);
+}, "Snaps to the snap position if the snap area covers the snapport on x on the right border.");
+
+// We use end alignment for this test so that we don't fall on a snap
+// position when the snap area just covers the snapport on the left border.
+test(() => {
+ document.getElementById("single").style.scrollSnapAlign = 'end';
+ one_target_scroller.scrollTo(200, 0);
+ assert_equals(one_target_scroller.scrollLeft, 200);
+ assert_equals(one_target_scroller.scrollTop, 0);
+}, "Snaps to the snap position if the snap area covers the snapport on x on the left border.");
+
+test(() => {
+ scroller_x.scrollTo(450, 0);
+ assert_equals(scroller_x.scrollLeft, 400);
+ assert_equals(scroller_x.scrollTop, 0);
+}, "Snaps to a snap area (400) that is closer than the position that reveals " +
+ "the space between snap areas (600) within the larger snap area on x.");
+
+test(() => {
+ scroller_y.scrollTo(0, 450);
+ assert_equals(scroller_y.scrollLeft, 0);
+ assert_equals(scroller_y.scrollTop, 400);
+}, "Snaps to a snap area (400) that is closer than the position that reveals " +
+ "the space between snap areas (600) within the larger snap area on y.");
+
+test(() => {
+ scroller_x.scrollTo(1650, 0);
+ assert_equals(scroller_x.scrollLeft, 1650);
+ assert_equals(scroller_x.scrollTop, 0);
+}, "Snap to current scroll position which is a valid snap position, as the " +
+ "snap area covers snapport on x and there is no intruding snap area.");
+
+test(() => {
+ scroller_y.scrollTo(0, 1650);
+ assert_equals(scroller_y.scrollLeft, 0);
+ assert_equals(scroller_y.scrollTop, 1650);
+}, "Snap to current scroll position which is a valid snap position, as the " +
+ "snap area covers snapport on y and there is no intruding snap area.");
+
+test(() => {
+ const maxScrollTop = scroller_y.scrollHeight - scroller_y.clientHeight;
+
+ // Scroll to the bottom edge which is a valid snap position that a large
+ // target element covers the snapport.
+ scroller_y.scrollTo(0, maxScrollTop);
+ assert_equals(scroller_y.scrollTop, maxScrollTop);
+
+ // Scroll to `the bottom edge + 1`.
+ scroller_y.scrollTo(0, maxScrollTop + 1);
+ assert_equals(scroller_y.scrollTop, maxScrollTop);
+}, "Don't snap back even if scrollTo tries to scroll to positions which are " +
+ "outside of the scroll range and if a snap target element covers the snaport");
+
+test(() => {
+ two_axes_scroller.scrollTo(10, 100);
+ assert_equals(two_axes_scroller.scrollLeft, 10);
+ assert_equals(two_axes_scroller.scrollTop, 200);
+}, "Snap to current scroll position on x as the area is covering x axis." +
+ "However, we snap to the specified snap position on y as the area is not " +
+ "covering y axis.");
+
+test(() => {
+ overlapping_scroller.scrollTo(200, 800);
+ assert_equals(overlapping_scroller.scrollLeft, 200);
+ assert_equals(overlapping_scroller.scrollTop, 800);
+}, "snap to current scroll position on y as the area is covering y axis, " +
+ "even though that area is not the only scroll area at the same position.");
+
+test(() => {
+ overlapping_scroller.scrollTo(800, 200);
+ assert_equals(overlapping_scroller.scrollLeft, 800);
+ assert_equals(overlapping_scroller.scrollTop, 200);
+}, "snap to current scroll position on x as the area is covering x axis, " +
+ "even though that area is not the only scroll area at the same position.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/overscroll-snap.html b/testing/web-platform/tests/css/css-scroll-snap/overscroll-snap.html
new file mode 100644
index 0000000000..13437492ca
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/overscroll-snap.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+ </head>
+ <body>
+ <style>
+ #scroller {
+ width: 200px;
+ height: 400px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+ background-color: blue;
+ }
+ #snap_target {
+ width: 100px;
+ height: 1942.5px;
+ scroll-snap-align: start;
+ background-color: pink;
+ }
+ </style>
+ <div id="scroller">
+ <div id="snap_target"></div>
+ </div>
+ <script>
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+ let scrollend_promise = new Promise((resolve) => {
+ scroller.addEventListener("scrollend", resolve);
+ });
+ let scroll_promise = new Promise((resolve) => {
+ scroller.addEventListener("scroll", resolve);
+ });
+ await new test_driver.Actions().scroll(0, 0, 0,
+ scroller.scrollHeight, { origin: scroller }).send();
+ await scroll_promise;
+ await scrollend_promise;
+ assert_approx_equals(scroller.scrollTop,
+ scroller.scrollHeight - scroller.clientHeight, 1,
+ "scroller is scrolled to its bottom and not its top.");
+ }, "snapport covered by snap area doesn't jump");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-computed.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-computed.html
new file mode 100644
index 0000000000..3bb0e740ac
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-computed.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap: getComputedStyle().scrollMarginBlock / scrollMarginInline</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#margin-longhands-logical">
+<meta name="assert" content="scroll-margin-block, scroll-margin-inline computed value is absolute length.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<body>
+<div id="target"></div>
+<style>
+ #target {
+ font-size: 40px;
+ }
+</style>
+<script>
+test_computed_value("scroll-margin-block-start", "10px");
+test_computed_value("scroll-margin-block-start", "calc(10px - 0.5em)", "-10px");
+
+test_computed_value("scroll-margin-block-end", "10px");
+test_computed_value("scroll-margin-block-end", "calc(10px - 0.5em)", "-10px");
+
+test_computed_value("scroll-margin-inline-start", "10px");
+test_computed_value("scroll-margin-inline-start", "calc(10px - 0.5em)", "-10px");
+
+test_computed_value("scroll-margin-inline-end", "10px");
+test_computed_value("scroll-margin-inline-end", "calc(10px - 0.5em)", "-10px");
+
+
+test_computed_value("scroll-margin-block", "10px");
+test_computed_value("scroll-margin-block", "calc(10px - 0.5em)", "-10px");
+test_computed_value("scroll-margin-block", "1px 2px");
+
+test_computed_value("scroll-margin-inline", "10px");
+test_computed_value("scroll-margin-inline", "calc(10px - 0.5em)", "-10px");
+test_computed_value("scroll-margin-inline", "1px 2px");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-invalid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-invalid.html
new file mode 100644
index 0000000000..b8a70b0d48
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-invalid.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-margin-block, scroll-margin-inline with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#margin-longhands-logical">
+<meta name="assert" content="scroll-margin-block, scroll-margin-inline support only the grammar '<length>{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("scroll-margin-block-start", "auto");
+test_invalid_value("scroll-margin-block-start", "20%");
+test_invalid_value("scroll-margin-block-start", "-30%");
+test_invalid_value("scroll-margin-block-start", "1px 2px");
+
+test_invalid_value("scroll-margin-block-end", "auto");
+test_invalid_value("scroll-margin-block-end", "20%");
+test_invalid_value("scroll-margin-block-end", "-30%");
+test_invalid_value("scroll-margin-block-end", "1px 2px");
+
+test_invalid_value("scroll-margin-inline-start", "auto");
+test_invalid_value("scroll-margin-inline-start", "20%");
+test_invalid_value("scroll-margin-inline-start", "-30%");
+test_invalid_value("scroll-margin-inline-start", "1px 2px");
+
+test_invalid_value("scroll-margin-inline-end", "auto");
+test_invalid_value("scroll-margin-inline-end", "20%");
+test_invalid_value("scroll-margin-inline-end", "-30%");
+test_invalid_value("scroll-margin-inline-end", "1px 2px");
+
+
+test_invalid_value("scroll-margin-block", "auto");
+test_invalid_value("scroll-margin-block", "20%");
+test_invalid_value("scroll-margin-block", "-30%");
+test_invalid_value("scroll-margin-block", "1px 2px 3px");
+
+test_invalid_value("scroll-margin-inline", "auto");
+test_invalid_value("scroll-margin-inline", "20%");
+test_invalid_value("scroll-margin-inline", "-30%");
+test_invalid_value("scroll-margin-inline", "1px 2px 3px");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-shorthand.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-shorthand.html
new file mode 100644
index 0000000000..04e1d42864
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-shorthand.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-margin-block, scroll-margin-inline set longhands</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#margin-longhands-logical">
+<meta name="assert" content="scroll-margin-block, scroll-margin-inline support the full grammar '<length>{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/shorthand-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_shorthand_value('scroll-margin-block', '10px 20px', {
+ 'scroll-margin-block-start': '10px',
+ 'scroll-margin-block-end': '20px'
+});
+
+test_shorthand_value('scroll-margin-block', '30px', {
+ 'scroll-margin-block-start': '30px',
+ 'scroll-margin-block-end': '30px'
+});
+
+test_shorthand_value('scroll-margin-inline', '50px 60px', {
+ 'scroll-margin-inline-start': '50px',
+ 'scroll-margin-inline-end': '60px'
+});
+
+test_shorthand_value('scroll-margin-inline', '-40px', {
+ 'scroll-margin-inline-start': '-40px',
+ 'scroll-margin-inline-end': '-40px'
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-valid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-valid.html
new file mode 100644
index 0000000000..e675eeb427
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-block-inline-valid.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-margin-block, scroll-margin-inline with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#margin-longhands-logical">
+<meta name="assert" content="scroll-margin-block, scroll-margin-inline support the full grammar '<length>{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("scroll-margin-block-start", "-10px");
+test_valid_value("scroll-margin-block-start", "calc(2em + 3ex)");
+
+test_valid_value("scroll-margin-block-end", "-10px");
+test_valid_value("scroll-margin-block-end", "calc(2em + 3ex)");
+
+test_valid_value("scroll-margin-inline-start", "-10px");
+test_valid_value("scroll-margin-inline-start", "calc(2em + 3ex)");
+
+test_valid_value("scroll-margin-inline-end", "-10px");
+test_valid_value("scroll-margin-inline-end", "calc(2em + 3ex)");
+
+
+test_valid_value("scroll-margin-block", "-10px");
+test_valid_value("scroll-margin-block", "calc(2em + 3ex)");
+test_valid_value("scroll-margin-block", "1px 2px");
+
+test_valid_value("scroll-margin-inline", "-10px");
+test_valid_value("scroll-margin-inline", "calc(2em + 3ex)");
+test_valid_value("scroll-margin-inline", "1px 2px");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-computed.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-computed.html
new file mode 100644
index 0000000000..ae7b6de5dd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-computed.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap: getComputedStyle().scrollMargin</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin">
+<meta name="assert" content="scroll-margin computed value is absolute length.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<body>
+<div id="target"></div>
+<style>
+ #target {
+ font-size: 40px;
+ }
+</style>
+<script>
+test_computed_value("scroll-margin-top", "10px");
+test_computed_value("scroll-margin-top", "calc(10px - 0.5em)", "-10px");
+
+
+test_computed_value("scroll-margin-right", "10px");
+test_computed_value("scroll-margin-right", "calc(10px - 0.5em)", "-10px");
+
+
+test_computed_value("scroll-margin-bottom", "10px");
+test_computed_value("scroll-margin-bottom", "calc(10px - 0.5em)", "-10px");
+
+
+test_computed_value("scroll-margin-left", "10px");
+test_computed_value("scroll-margin-left", "calc(10px - 0.5em)", "-10px");
+
+
+test_computed_value("scroll-margin", "10px");
+test_computed_value("scroll-margin", "calc(10px - 0.5em)", "-10px");
+
+test_computed_value("scroll-margin", "1px 2px");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-invalid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-invalid.html
new file mode 100644
index 0000000000..97beb0d295
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-invalid.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-margin with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin">
+<meta name="assert" content="scroll-margin supports only the grammar '<length>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("scroll-margin-top", "auto");
+test_invalid_value("scroll-margin-top", "20%");
+test_invalid_value("scroll-margin-top", "-30%");
+test_invalid_value("scroll-margin-top", "1px 2px");
+
+
+test_invalid_value("scroll-margin-right", "auto");
+test_invalid_value("scroll-margin-right", "20%");
+test_invalid_value("scroll-margin-right", "-30%");
+test_invalid_value("scroll-margin-right", "1px 2px");
+
+
+test_invalid_value("scroll-margin-bottom", "auto");
+test_invalid_value("scroll-margin-bottom", "20%");
+test_invalid_value("scroll-margin-bottom", "-30%");
+test_invalid_value("scroll-margin-bottom", "1px 2px");
+
+
+test_invalid_value("scroll-margin-left", "auto");
+test_invalid_value("scroll-margin-left", "20%");
+test_invalid_value("scroll-margin-left", "-30%");
+test_invalid_value("scroll-margin-left", "1px 2px");
+
+
+test_invalid_value("scroll-margin", "auto");
+test_invalid_value("scroll-margin", "20%");
+test_invalid_value("scroll-margin", "-30%");
+test_invalid_value("scroll-margin", "1px 2px 3px 4px 5px");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-shorthand.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-shorthand.html
new file mode 100644
index 0000000000..96a4595226
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-shorthand.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-margin sets longhands</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin">
+<meta name="assert" content="scroll-margin supports the full grammar '<length>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/shorthand-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_shorthand_value('scroll-margin', '10px', {
+ 'scroll-margin-top': '10px',
+ 'scroll-margin-right': '10px',
+ 'scroll-margin-bottom': '10px',
+ 'scroll-margin-left': '10px'
+});
+
+test_shorthand_value('scroll-margin', '30px 20px', {
+ 'scroll-margin-top': '30px',
+ 'scroll-margin-right': '20px',
+ 'scroll-margin-bottom': '30px',
+ 'scroll-margin-left': '20px'
+});
+
+test_shorthand_value('scroll-margin', '1px 2px 3px', {
+ 'scroll-margin-top': '1px',
+ 'scroll-margin-right': '2px',
+ 'scroll-margin-bottom': '3px',
+ 'scroll-margin-left': '2px'
+});
+
+test_shorthand_value('scroll-margin', '-1px 2px 3px 0', {
+ 'scroll-margin-top': '-1px',
+ 'scroll-margin-right': '2px',
+ 'scroll-margin-bottom': '3px',
+ 'scroll-margin-left': '0px'
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-valid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-valid.html
new file mode 100644
index 0000000000..be34998691
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-margin-valid.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-margin with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin">
+<meta name="assert" content="scroll-margin supports the full grammar '<length>{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("scroll-margin-top", "0", "0px");
+test_valid_value("scroll-margin-top", "-10px");
+test_valid_value("scroll-margin-top", "calc(2em + 3ex)");
+
+
+test_valid_value("scroll-margin-right", "0", "0px");
+test_valid_value("scroll-margin-right", "-10px");
+test_valid_value("scroll-margin-right", "calc(2em + 3ex)");
+
+
+test_valid_value("scroll-margin-bottom", "0", "0px");
+test_valid_value("scroll-margin-bottom", "-10px");
+test_valid_value("scroll-margin-bottom", "calc(2em + 3ex)");
+
+
+test_valid_value("scroll-margin-left", "0", "0px");
+test_valid_value("scroll-margin-left", "-10px");
+test_valid_value("scroll-margin-left", "calc(2em + 3ex)");
+
+
+test_valid_value("scroll-margin", "0", "0px");
+test_valid_value("scroll-margin", "-10px");
+test_valid_value("scroll-margin", "calc(2em + 3ex)");
+
+test_valid_value("scroll-margin", "1px 2px");
+test_valid_value("scroll-margin", "1px 2px 3px");
+test_valid_value("scroll-margin", "1px 2px 3px 4px");
+test_valid_value("scroll-margin", "0 0 0 0", "0px");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-computed.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-computed.html
new file mode 100644
index 0000000000..6a66110cda
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-computed.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap: getComputedStyle().scrollPaddingBlock / scrollPaddingInline</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#padding-longhands-logical">
+<meta name="assert" content="scroll-padding-block, scroll-padding-inline computed value is per side, either the keyword auto or a computed <length-percentage> value.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<body>
+<div id="target"></div>
+<style>
+ #target {
+ font-size: 40px;
+ }
+</style>
+<script>
+test_computed_value("scroll-padding-block-start", "auto");
+test_computed_value("scroll-padding-block-start", "10px");
+test_computed_value("scroll-padding-block-start", "20%");
+test_computed_value("scroll-padding-block-start", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-block-start", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-block-start", "calc(50% + 60px)");
+
+test_computed_value("scroll-padding-block-end", "auto");
+test_computed_value("scroll-padding-block-end", "10px");
+test_computed_value("scroll-padding-block-end", "20%");
+test_computed_value("scroll-padding-block-end", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-block-end", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-block-end", "calc(50% + 60px)");
+
+test_computed_value("scroll-padding-inline-start", "auto");
+test_computed_value("scroll-padding-inline-start", "10px");
+test_computed_value("scroll-padding-inline-start", "20%");
+test_computed_value("scroll-padding-inline-start", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-inline-start", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-inline-start", "calc(50% + 60px)");
+
+test_computed_value("scroll-padding-inline-end", "auto");
+test_computed_value("scroll-padding-inline-end", "10px");
+test_computed_value("scroll-padding-inline-end", "20%");
+test_computed_value("scroll-padding-inline-end", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-inline-end", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-inline-end", "calc(50% + 60px)");
+
+
+test_computed_value("scroll-padding-block", "auto");
+test_computed_value("scroll-padding-block", "10px");
+test_computed_value("scroll-padding-block", "20%");
+test_computed_value("scroll-padding-block", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-block", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-block", "calc(50% + 60px)");
+test_computed_value("scroll-padding-block", "1px 2px");
+test_computed_value("scroll-padding-block", "1px auto");
+test_computed_value("scroll-padding-block", "auto auto", "auto");
+
+test_computed_value("scroll-padding-inline", "auto");
+test_computed_value("scroll-padding-inline", "10px");
+test_computed_value("scroll-padding-inline", "20%");
+test_computed_value("scroll-padding-inline", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-inline", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-inline", "calc(50% + 60px)");
+test_computed_value("scroll-padding-inline", "1px 2px");
+test_computed_value("scroll-padding-inline", "1px auto");
+test_computed_value("scroll-padding-inline", "auto auto", "auto");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-invalid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-invalid.html
new file mode 100644
index 0000000000..da995cfcc0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-invalid.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-padding-block, scroll-padding-inline with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#padding-longhands-logical">
+<meta name="assert" content="scroll-padding-block, scroll-padding-inline supports only the grammar '[ auto | <length-percentage> ]{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("scroll-padding-block-start", "none");
+test_invalid_value("scroll-padding-block-start", "-10px");
+test_invalid_value("scroll-padding-block-start", "10px 20%");
+test_invalid_value("scroll-padding-block-start", "fit-content");
+
+test_invalid_value("scroll-padding-block-end", "none");
+test_invalid_value("scroll-padding-block-end", "-10px");
+test_invalid_value("scroll-padding-block-end", "10px 20%");
+test_invalid_value("scroll-padding-block-end", "max-content");
+
+test_invalid_value("scroll-padding-inline-start", "none");
+test_invalid_value("scroll-padding-inline-start", "-10px");
+test_invalid_value("scroll-padding-inline-start", "10px 20%");
+test_invalid_value("scroll-padding-inline-start", "min-content");
+
+test_invalid_value("scroll-padding-inline-end", "none");
+test_invalid_value("scroll-padding-inline-end", "-10px");
+test_invalid_value("scroll-padding-inline-end", "10px 20%");
+test_invalid_value("scroll-padding-inline-end", "max-content");
+
+
+test_invalid_value("scroll-padding-block", "none");
+test_invalid_value("scroll-padding-block", "-10px");
+test_invalid_value("scroll-padding-block", "-20%");
+test_invalid_value("scroll-padding-block", "calc(auto)");
+test_invalid_value("scroll-padding-block", "10px 20px 30px 40px 50px");
+test_invalid_value("scroll-padding-block", "fit-content");
+test_invalid_value("scroll-padding-block", "max-content");
+test_invalid_value("scroll-padding-block", "min-content");
+
+test_invalid_value("scroll-padding-inline", "none");
+test_invalid_value("scroll-padding-inline", "-10px");
+test_invalid_value("scroll-padding-inline", "-20%");
+test_invalid_value("scroll-padding-inline", "calc(auto)");
+test_invalid_value("scroll-padding-inline", "10px 20px 30px 40px 50px");
+test_invalid_value("scroll-padding-inline", "fit-content");
+test_invalid_value("scroll-padding-inline", "max-content");
+test_invalid_value("scroll-padding-inline", "min-content");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-shorthand.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-shorthand.html
new file mode 100644
index 0000000000..491cfa9e96
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-shorthand.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-padding-block, scroll-padding-inline set longhands</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#padding-longhands-logical">
+<meta name="assert" content="scroll-padding-block, scroll-padding-inline support the full grammar '[ auto | <length-percentage> ]{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/shorthand-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_shorthand_value('scroll-padding-block', 'auto 10px', {
+ 'scroll-padding-block-start': 'auto',
+ 'scroll-padding-block-end': '10px'
+});
+
+test_shorthand_value('scroll-padding-block', '20%', {
+ 'scroll-padding-block-start': '20%',
+ 'scroll-padding-block-end': '20%'
+});
+
+test_shorthand_value('scroll-padding-inline', '10px auto', {
+ 'scroll-padding-inline-start': '10px',
+ 'scroll-padding-inline-end': 'auto'
+});
+
+test_shorthand_value('scroll-padding-inline', '0%', {
+ 'scroll-padding-inline-start': '0%',
+ 'scroll-padding-inline-end': '0%'
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-valid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-valid.html
new file mode 100644
index 0000000000..a932bb6393
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-block-inline-valid.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-padding-block, scroll-padding-inline with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#padding-longhands-logical">
+<meta name="assert" content="scroll-padding-block, scroll-padding-inline supports the full grammar '[ auto | <length-percentage> ]{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("scroll-padding-block-start", "auto");
+test_valid_value("scroll-padding-block-start", "10px");
+test_valid_value("scroll-padding-block-start", "20%");
+test_valid_value("scroll-padding-block-start", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-block-start", "calc(50% + 60px)");
+
+test_valid_value("scroll-padding-block-end", "auto");
+test_valid_value("scroll-padding-block-end", "10px");
+test_valid_value("scroll-padding-block-end", "20%");
+test_valid_value("scroll-padding-block-end", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-block-end", "calc(50% + 60px)");
+
+test_valid_value("scroll-padding-inline-start", "auto");
+test_valid_value("scroll-padding-inline-start", "10px");
+test_valid_value("scroll-padding-inline-start", "20%");
+test_valid_value("scroll-padding-inline-start", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-inline-start", "calc(50% + 60px)");
+
+test_valid_value("scroll-padding-inline-end", "auto");
+test_valid_value("scroll-padding-inline-end", "10px");
+test_valid_value("scroll-padding-inline-end", "20%");
+test_valid_value("scroll-padding-inline-end", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-inline-end", "calc(50% + 60px)");
+
+
+test_valid_value("scroll-padding-block", "auto");
+test_valid_value("scroll-padding-block", "10px");
+test_valid_value("scroll-padding-block", "20%");
+test_valid_value("scroll-padding-block", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-block", "calc(50% + 60px)");
+test_valid_value("scroll-padding-block", "1px 2px");
+test_valid_value("scroll-padding-block", "1px auto");
+test_valid_value("scroll-padding-block", "auto auto", "auto");
+
+test_valid_value("scroll-padding-inline", "auto");
+test_valid_value("scroll-padding-inline", "10px");
+test_valid_value("scroll-padding-inline", "20%");
+test_valid_value("scroll-padding-inline", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-inline", "calc(50% + 60px)");
+test_valid_value("scroll-padding-inline", "1px 2px");
+test_valid_value("scroll-padding-inline", "1px auto");
+test_valid_value("scroll-padding-inline", "auto auto", "auto");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-computed.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-computed.html
new file mode 100644
index 0000000000..f638138a7f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-computed.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap: getComputedStyle().scrollPadding</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding">
+<meta name="assert" content="scroll-padding computed value is per side, either the keyword auto or a computed <length-percentage> value.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<body>
+<div id="target"></div>
+<style>
+ #target {
+ font-size: 40px;
+ }
+</style>
+<script>
+test_computed_value("scroll-padding-top", "auto");
+test_computed_value("scroll-padding-top", "0", "0px");
+test_computed_value("scroll-padding-top", "10px");
+test_computed_value("scroll-padding-top", "20%");
+test_computed_value("scroll-padding-top", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-top", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-top", "calc(50% + 60px)");
+
+
+test_computed_value("scroll-padding-right", "auto");
+test_computed_value("scroll-padding-right", "0", "0px");
+test_computed_value("scroll-padding-right", "10px");
+test_computed_value("scroll-padding-right", "20%");
+test_computed_value("scroll-padding-right", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-right", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-right", "calc(50% + 60px)");
+
+
+test_computed_value("scroll-padding-bottom", "auto");
+test_computed_value("scroll-padding-bottom", "0", "0px");
+test_computed_value("scroll-padding-bottom", "10px");
+test_computed_value("scroll-padding-bottom", "20%");
+test_computed_value("scroll-padding-bottom", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-bottom", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-bottom", "calc(50% + 60px)");
+
+
+test_computed_value("scroll-padding-left", "auto");
+test_computed_value("scroll-padding-left", "0", "0px");
+test_computed_value("scroll-padding-left", "10px");
+test_computed_value("scroll-padding-left", "20%");
+test_computed_value("scroll-padding-left", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding-left", "calc(10px - 0.5em)", "0px");
+test_computed_value("scroll-padding-left", "calc(50% + 60px)");
+
+
+test_computed_value("scroll-padding", "auto");
+test_computed_value("scroll-padding", "10px");
+test_computed_value("scroll-padding", "0", "0px");
+test_computed_value("scroll-padding", "20%");
+test_computed_value("scroll-padding", "calc(10px + 0.5em)", "30px");
+test_computed_value("scroll-padding", "calc(10px - 0.5em)", "0px");
+
+test_computed_value("scroll-padding", "1px 2px");
+test_computed_value("scroll-padding", "1px 2px 3%");
+test_computed_value("scroll-padding", "1px 2px 3% 4px");
+test_computed_value("scroll-padding", "1px auto");
+test_computed_value("scroll-padding", "0 0 0 0", "0px");
+test_computed_value("scroll-padding", "auto auto auto auto", "auto");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-invalid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-invalid.html
new file mode 100644
index 0000000000..c805ee2e55
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-invalid.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-padding with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding">
+<meta name="assert" content="scroll-padding supports only the grammar '[ <length-percentage> | auto ]{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("scroll-padding-top", "20");
+test_invalid_value("scroll-padding-top", "-20px");
+test_invalid_value("scroll-padding-top", "none");
+test_invalid_value("scroll-padding-top", "10px 20%");
+test_invalid_value("scroll-padding-top", "fit-content");
+
+
+test_invalid_value("scroll-padding-right", "20");
+test_invalid_value("scroll-padding-right", "-20px");
+test_invalid_value("scroll-padding-right", "none");
+test_invalid_value("scroll-padding-right", "10px 20%");
+test_invalid_value("scroll-padding-right", "max-content");
+
+
+test_invalid_value("scroll-padding-bottom", "20");
+test_invalid_value("scroll-padding-bottom", "-20px");
+test_invalid_value("scroll-padding-bottom", "none");
+test_invalid_value("scroll-padding-bottom", "10px 20%");
+test_invalid_value("scroll-padding-bottom", "min-content");
+
+
+test_invalid_value("scroll-padding-left", "20");
+test_invalid_value("scroll-padding-left", "-20px");
+test_invalid_value("scroll-padding-left", "none");
+test_invalid_value("scroll-padding-left", "10px 20%");
+test_invalid_value("scroll-padding-left", "fit-content");
+
+
+test_invalid_value("scroll-padding", "20");
+test_invalid_value("scroll-padding", "-20px");
+test_invalid_value("scroll-padding", "none");
+test_invalid_value("scroll-padding", "calc(auto)");
+test_invalid_value("scroll-padding", "10px 20px 30px 40px 50px");
+test_invalid_value("scroll-padding", "fit-content");
+test_invalid_value("scroll-padding", "max-content");
+test_invalid_value("scroll-padding", "min-content");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-shorthand.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-shorthand.html
new file mode 100644
index 0000000000..2162c7f10d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-shorthand.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-padding sets longhands</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding">
+<meta name="assert" content="scroll-padding supports the full grammar '[ <length-percentage> | auto ]{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/shorthand-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_shorthand_value('scroll-padding', '10px', {
+ 'scroll-padding-top': '10px',
+ 'scroll-padding-right': '10px',
+ 'scroll-padding-bottom': '10px',
+ 'scroll-padding-left': '10px'
+});
+
+test_shorthand_value('scroll-padding', 'auto 20px', {
+ 'scroll-padding-top': 'auto',
+ 'scroll-padding-right': '20px',
+ 'scroll-padding-bottom': 'auto',
+ 'scroll-padding-left': '20px'
+});
+
+test_shorthand_value('scroll-padding', '1px 2px 3px', {
+ 'scroll-padding-top': '1px',
+ 'scroll-padding-right': '2px',
+ 'scroll-padding-bottom': '3px',
+ 'scroll-padding-left': '2px'
+});
+
+test_shorthand_value('scroll-padding', '1% 2px 3px 0', {
+ 'scroll-padding-top': '1%',
+ 'scroll-padding-right': '2px',
+ 'scroll-padding-bottom': '3px',
+ 'scroll-padding-left': '0px'
+});
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-valid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-valid.html
new file mode 100644
index 0000000000..0e7c86b12b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-padding-valid.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-padding with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding">
+<meta name="assert" content="scroll-padding supports the full grammar '[ <length-percentage> | auto ]{1,4}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("scroll-padding-top", "auto");
+test_valid_value("scroll-padding-top", "0", "0px");
+test_valid_value("scroll-padding-top", "10px");
+test_valid_value("scroll-padding-top", "20%");
+test_valid_value("scroll-padding-top", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-top", "calc(50% + 60px)");
+
+
+test_valid_value("scroll-padding-right", "auto");
+test_valid_value("scroll-padding-right", "0", "0px");
+test_valid_value("scroll-padding-right", "10px");
+test_valid_value("scroll-padding-right", "20%");
+test_valid_value("scroll-padding-right", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-right", "calc(50% + 60px)");
+
+
+test_valid_value("scroll-padding-bottom", "auto");
+test_valid_value("scroll-padding-bottom", "0", "0px");
+test_valid_value("scroll-padding-bottom", "10px");
+test_valid_value("scroll-padding-bottom", "20%");
+test_valid_value("scroll-padding-bottom", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-bottom", "calc(50% + 60px)");
+
+
+test_valid_value("scroll-padding-left", "auto");
+test_valid_value("scroll-padding-left", "0", "0px");
+test_valid_value("scroll-padding-left", "10px");
+test_valid_value("scroll-padding-left", "20%");
+test_valid_value("scroll-padding-left", "calc(2em + 3ex)");
+test_valid_value("scroll-padding-left", "calc(50% + 60px)");
+
+
+test_valid_value("scroll-padding", "auto");
+test_valid_value("scroll-padding", "10px");
+test_valid_value("scroll-padding", "0", "0px");
+test_valid_value("scroll-padding", "20%");
+test_valid_value("scroll-padding", "calc(2em + 3ex)");
+
+test_valid_value("scroll-padding", "1px 2px");
+test_valid_value("scroll-padding", "1px 2px 3%");
+test_valid_value("scroll-padding", "1px 2px 3% 4px");
+test_valid_value("scroll-padding", "1px auto");
+test_valid_value("scroll-padding", "0 0 0 0", "0px");
+test_valid_value("scroll-padding", "auto auto auto auto", "auto");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-computed.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-computed.html
new file mode 100644
index 0000000000..daed998ef2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-computed.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap: getComputedStyle().scrollSnapAlign</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-align">
+<meta name="assert" content="scroll-snap-align computed value is as specified.">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<body>
+<div id="target"></div>
+<script>
+test_computed_value("scroll-snap-align", "none");
+test_computed_value("scroll-snap-align", "start");
+test_computed_value("scroll-snap-align", "end");
+test_computed_value("scroll-snap-align", "center");
+
+test_computed_value("scroll-snap-align", "start none");
+test_computed_value("scroll-snap-align", "center end");
+test_computed_value("scroll-snap-align", "start start", "start");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-invalid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-invalid.html
new file mode 100644
index 0000000000..9a1eeb77bb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-invalid.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-snap-align with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-align">
+<meta name="assert" content="scroll-snap-align supports only the grammar '[ none | start | end | center ]{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("scroll-snap-align", "auto");
+
+test_invalid_value("scroll-snap-align", "start invalid");
+
+test_invalid_value("scroll-snap-align", "start end center");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-valid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-valid.html
new file mode 100644
index 0000000000..0201448825
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-align-valid.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-snap-align with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-align">
+<meta name="assert" content="scroll-snap-align supports the full grammar '[ none | start | end | center ]{1,2}'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("scroll-snap-align", "none");
+test_valid_value("scroll-snap-align", "start");
+test_valid_value("scroll-snap-align", "end");
+test_valid_value("scroll-snap-align", "center");
+
+test_valid_value("scroll-snap-align", "start none");
+test_valid_value("scroll-snap-align", "center end");
+test_valid_value("scroll-snap-align", "start start", "start");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-computed.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-computed.html
new file mode 100644
index 0000000000..49b369a0fe
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-computed.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap: getComputedStyle().scrollSnapStop</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-stop">
+<meta name="assert" content="scroll-snap-stop computed value is as specified.">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<body>
+<div id="target"></div>
+<script>
+test_computed_value("scroll-snap-stop", "normal");
+test_computed_value("scroll-snap-stop", "always");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.html
new file mode 100644
index 0000000000..67feda0ca7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-invalid.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-snap-stop with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-stop">
+<meta name="assert" content="scroll-snap-stop supports only the grammar 'normal | always'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("scroll-snap-stop", "auto");
+
+test_invalid_value("scroll-snap-stop", "normal always");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-valid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-valid.html
new file mode 100644
index 0000000000..a59caff396
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-stop-valid.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-snap-stop with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-stop">
+<meta name="assert" content="scroll-snap-stop supports the full grammar 'normal | always'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("scroll-snap-stop", "normal");
+test_valid_value("scroll-snap-stop", "always");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-computed.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-computed.html
new file mode 100644
index 0000000000..660a1fee06
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-computed.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap: getComputedStyle().scrollSnapType</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-type">
+<meta name="assert" content="scroll-snap-type computed value is as specified.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/computed-testcommon.js"></script>
+</head>
+<body>
+<div id="target"></div>
+<script>
+test_computed_value("scroll-snap-type", "none");
+
+test_computed_value("scroll-snap-type", "x");
+test_computed_value("scroll-snap-type", "y");
+test_computed_value("scroll-snap-type", "block");
+test_computed_value("scroll-snap-type", "inline");
+test_computed_value("scroll-snap-type", "both");
+
+test_computed_value("scroll-snap-type", "y mandatory");
+test_computed_value("scroll-snap-type", "inline proximity", "inline");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-invalid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-invalid.html
new file mode 100644
index 0000000000..6177ff3baf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-invalid.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-snap-type with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-type">
+<meta name="assert" content="scroll-snap-type supports only the grammar 'none | [ x | y | block | inline | both ] [ mandatory | proximity ]?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_invalid_value("scroll-snap-type", "auto");
+
+test_invalid_value("scroll-snap-type", "x y");
+test_invalid_value("scroll-snap-type", "block mandatory inline");
+
+test_invalid_value("scroll-snap-type", "both none");
+test_invalid_value("scroll-snap-type", "mandatory");
+test_invalid_value("scroll-snap-type", "proximity");
+test_invalid_value("scroll-snap-type", "mandatory inline");
+test_invalid_value("scroll-snap-type", "proximity both");
+test_invalid_value("scroll-snap-type", "mandatory x");
+test_invalid_value("scroll-snap-type", "proximity y");
+test_invalid_value("scroll-snap-type", "mandatory block");
+test_invalid_value("scroll-snap-type", "proximity mandatory");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-valid.html b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-valid.html
new file mode 100644
index 0000000000..ca995770f4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/parsing/scroll-snap-type-valid.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Snap Test: scroll-snap-type with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-snap-type">
+<meta name="assert" content="scroll-snap-type supports the full grammar 'none | [ x | y | block | inline | both ] [ mandatory | proximity ]?'.">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/support/parsing-testcommon.js"></script>
+</head>
+<body>
+<script>
+test_valid_value("scroll-snap-type", "none");
+
+test_valid_value("scroll-snap-type", "x");
+test_valid_value("scroll-snap-type", "y");
+test_valid_value("scroll-snap-type", "block");
+test_valid_value("scroll-snap-type", "inline");
+test_valid_value("scroll-snap-type", "both");
+
+test_valid_value("scroll-snap-type", "y mandatory");
+test_valid_value("scroll-snap-type", "block mandatory");
+test_valid_value("scroll-snap-type", "both mandatory");
+test_valid_value("scroll-snap-type", "inline proximity", "inline"); // The shortest serialization is preferable
+test_valid_value("scroll-snap-type", "x proximity", "x");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/resnap-on-snap-alignment-change.html b/testing/web-platform/tests/css/css-scroll-snap/resnap-on-snap-alignment-change.html
new file mode 100644
index 0000000000..e4648b1d79
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/resnap-on-snap-alignment-change.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<title>
+ Resnap when the current snap position is no longer a valid snap target.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-scope"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/css/css-scroll-snap/support/common.js"></script>
+<style>
+ html {
+ scroll-snap-type: y mandatory;
+ }
+ body {
+ margin: 0;
+ }
+ .snap {
+ scroll-snap-align: center;
+ height: 100vh;
+ margin: 0;
+ }
+</style>
+<body onload = "runTest()">
+<div id = "card1" class="snap">ONE</div>
+<div id = "card2" class="snap">TWO</div>
+<div id = "card3" class="snap">THREE</div>
+</body>
+<script type="text/javascript">
+ function runTest() {
+ promise_test(async t => {
+ const scroller = document.scrollingElement;
+ // scroll to card THREE
+ let expectedSnapPosition = card3.offsetTop;
+ scroller.scrollTo(0, expectedSnapPosition);
+ assert_equals(scroller.scrollTop, expectedSnapPosition,
+ 'Aligned with card THREE before style change');
+
+ // Snap to card TWO after invalidating card THREE as a snap target.
+ card3.style.scrollSnapAlign = 'none center';
+ await waitForNextFrame();
+ expectedSnapPosition = card2.offsetTop;
+ assert_equals(scroller.scrollTop, expectedSnapPosition,
+ 'Aligned with card TWO after style change');
+ }, 'Resnap when the current snap position is no longer a valid snap target');
+ }
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-margin-visibility-check.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-margin-visibility-check.html
new file mode 100644
index 0000000000..b41ccb36fd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-margin-visibility-check.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-margin">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1708303">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+body { margin: 0 }
+#scroller {
+ height: 500px;
+ width: 500px;
+ scrollbar-width: none;
+ overflow: scroll;
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+ scroll-margin: 100px;
+ margin-left: 510px;
+}
+</style>
+
+<div id="scroller">
+ <div style="width: 450px; height: 450px;"></div>
+ <div tabindex="0" id="target"></div>
+ <div style="width: 2000px; height: 2000px;"></div>
+</div>
+
+<script>
+let scroller = document.getElementById("scroller");
+let target = document.getElementById("target");
+promise_test(async function() {
+ scroller.scrollTo(0, 0);
+ await new Promise(resolve => {
+ scroller.addEventListener("scroll", resolve, { once: true });
+ document.getElementById("target").focus();
+ });
+ assert_true(scroller.scrollTop > 0, "Visibility check should not account for margin");
+ assert_true(scroller.scrollLeft > 0, "Visibility check should not account for margin");
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-margin.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-margin.html
new file mode 100644
index 0000000000..e6ce4ac49c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-margin.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-margin" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+#target {
+ width: 300px;
+ height: 300px;
+ background-color: blue;
+}
+#another-target {
+ width: 300px;
+ height: 300px;
+ top: 400px;
+ left: 400px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+</style>
+
+<div id="scroller">
+ <div style="width: 2000px; height: 2000px;"></div>
+ <div id="target"></div>
+ <div id="another-target"></div>
+</div>
+
+<script>
+test(() => {
+ target.style.scrollSnapAlign = "start";
+ target.style.scrollMargin = "100px";
+ target.style.left = "300px";
+ target.style.top = "300px";
+
+ scroller.scrollTo(0, 0);
+ // `target position (300px, 300px)` - `margin (100px, 100px)`.
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 200);
+
+ target.style.scrollSnapAlign = "end";
+
+ // `target position (300px, 300px)` + `target size (300px, 300px) +
+ // `margin (100px, 100px) - `scroller size (500px, 500px)`.
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 200);
+}, "Snaps to the positions adjusted by scroll-margin");
+
+test(() => {
+ target.style.left = "0px";
+ target.style.top = "0px";
+ target.style.scrollSnapAlign = "start";
+
+ // Since the target is at (0px, 0px) in the scroll port, the added margin
+ // should not be considered, and the snap points for this snap area should be
+ // the closest points in the scroll port (i.e x=0 or y=0).
+ target.style.scrollMargin = "200px";
+
+ // Distance from target without margin:
+ // `scroll position (150px, 150px)` - `target position (0px, 0px)`
+ // = (150px, 150px)
+ //
+ // Distance from target with margin:
+ // `scroll position (150px, 150px)` - [`target position (0px, 0px)` -
+ // `target margin (200px, 200px)`]
+ // = (350px, 350px)
+ //
+ // Distance from other target:
+ // `other target position (400px, 400px)` - `scroll position (150px, 150px)`
+ // = (250px, 250px)
+ //
+ // Therefore if the "out-of-scrollport" scroll-margin contributes to the
+ // calculation, then the other target would be snapped to. However if the
+ // scroll-margin is not considered, then the (0px, 0px) target should be
+ // snapped to.
+ scroller.scrollTo(150, 150);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+}, "scroll-margin doesn't contribute to the snap position of the element " +
+ "if it's outside of the scroll port");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.tentative.html
new file mode 100644
index 0000000000..df4fc20ec8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-on-large-element-not-covering-snapport.tentative.html
@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<title>
+ A test case that scrolling to a point on large element where the snap area
+ doesn't cover over the snapport
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ width: 100%;
+}
+#scroller {
+ left: 10px;
+ height: 200px;
+ width: 100px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ scroll-snap-type: y mandatory;
+}
+.snap {
+ scroll-snap-align: start;
+ background-color: green;
+}
+.target {
+ background-color: red;
+ border-radius: 100%;
+ height: 88px;
+ top: 536px;
+}
+</style>
+<div id="scroller">
+ <div style="height: 2000px;"></div>
+ <div class="snap" style="top: 0px; height: 40px;">1</div>
+ <div class="snap" style="top: 40px; height: 40px;">2</div>
+ <div class="snap" style="top: 80px; height: 1000px;">3</div>
+ <div class="snap" style="top: 1080px; height: 40px;">4</div>
+ <div class="snap" style="top: 1120px; height: 40px;">5</div>
+ <div class="target"></div>
+</div>
+<script>
+test(() => {
+ const scroller = document.getElementById("scroller");
+
+ scroller.scrollBy(0, 10);
+ // Snap to the first snap point.
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 40);
+
+ scroller.scrollBy(0, 10);
+ // Snap to the second snap point.
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 80);
+
+ scroller.scrollBy(0, 100);
+ // Snap to the given scrolling position since the third snap target element
+ // is larger than the snapport.
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 180);
+
+ scroller.scrollBy(0, 100);
+ // Again, snap to the given scrolling position.
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 280);
+
+ // Scroll to a point where the third target element is still covering over the
+ // snapport.
+ scroller.scrollBy(0, 600);
+ assert_equals(scroller.scrollLeft, 0);
+ // Again, snap to the given scrolling position.
+ assert_equals(scroller.scrollTop, 880);
+
+ // Scroll to a point where the third target element is no longer convering
+ // over the snapport, rather the forth snap point is in the snapport.
+ scroller.scrollBy(0, 10);
+ // Now, snap to the forth snap point.
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 1080);
+
+ // Scroll back a bit.
+ scroller.scrollBy(0, -10);
+ // This should snap to the bottom of the 3rd snap area and not all the way
+ // back up to its top.
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 880);
+}, "snaps to bottom edge of large snap area that doesn't cover the snap port.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-padding-and-margin.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-padding-and-margin.html
new file mode 100644
index 0000000000..97d30c702d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-padding-and-margin.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-margin">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1708303">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+body { margin: 0 }
+#scroller {
+ height: 500px;
+ width: 500px;
+ scroll-padding: 10px;
+ scrollbar-width: none;
+ overflow: hidden;
+}
+#target {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+ scroll-margin: 10px;
+ margin-left: 1000px;
+}
+</style>
+
+<div id="scroller">
+ <div style="width: 2000px; height: 2000px;"></div>
+ <div id="target"></div>
+ <div style="width: 2000px; height: 2000px;"></div>
+</div>
+
+<script>
+let scroller = document.getElementById("scroller");
+let target = document.getElementById("target");
+test(() => {
+ scroller.scrollTo(0, 0);
+ target.scrollIntoView({ block: "start", inline: "start" });
+
+ assert_equals(scroller.scrollTop, 2000 - 20, "Top should account for margin + padding");
+ assert_equals(scroller.scrollLeft, 1000 - 20, "Left should account for margin + padding");
+});
+
+test(() => {
+ scroller.scrollTo(0, 0);
+ target.scrollIntoView({ block: "end", inline: "end" });
+ assert_equals(scroller.scrollTop, 2000 - 500 + 100 + 20, "Top should account for margin + padding");
+ assert_equals(scroller.scrollLeft, 1000 - 500 + 100 + 20, "Left should account for margin + padding");
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-padding.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-padding.html
new file mode 100644
index 0000000000..0c637ed6db
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-padding.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+#target {
+ width: 300px;
+ height: 300px;
+ background-color: blue;
+}
+</style>
+
+<div id="scroller">
+ <div style="width: 2000px; height: 2000px;"></div>
+ <div id="target"></div>
+</div>
+
+<script>
+test(() => {
+ scroller.style.scrollPadding = "100px";
+
+ target.style.scrollSnapAlign = "start";
+ target.style.left = "300px";
+ target.style.top = "300px";
+
+ scroller.scrollTo(0, 0);
+ // `target position (300px, 300px)` - `padding (100px, 100px)`.
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 200);
+
+ target.style.scrollSnapAlign = "end";
+
+ // `target position (300px, 300px)` + `target size (300px, 300px) +
+ // `padding (100px, 100px) - `scroller size (500px, 500px)`.
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 200);
+}, "Snaps to the positions adjusted by scroll-padding");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-nested-snap-area-layout-changed.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-nested-snap-area-layout-changed.tentative.html
new file mode 100644
index 0000000000..be0bd5c4a5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-nested-snap-area-layout-changed.tentative.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+</head>
+<body>
+ <style>
+ #scroller {
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+ height: 200px;
+ width: 200px;
+ border: solid 1px;
+ position:absolute;
+ }
+ .snap_area {
+ position: absolute;
+ width: 100px;
+ left: calc(50% - 50px);
+ }
+ #outer_snap_area {
+ scroll-snap-align: start;
+ height: 1000px;
+ background-color: blue;
+ }
+ #inner_snap_area {
+ scroll-snap-align: start;
+ height: 100px;
+ top: 100px;
+ background-color: yellow;
+ }
+ </style>
+ <div id="scroller">
+ <div id="outer_snap_area" class="snap_area"></div>
+ <div id="inner_snap_area" class="snap_area"></div>
+ </div>
+ <script>
+ async function reset() {
+ inner_snap_area.style.height = "100px";
+ await waitForCompositorCommit();
+ }
+
+ let scroller = document.getElementById("scroller");
+ promise_test(async (t) => {
+ await reset();
+ await resetTargetScrollState(t, scroller);
+ assert_equals(scroller.scrollTop, 0, "test precondition: scroller is " +
+ "not scrolled");
+ let scrollend_promise = waitForScrollendEventNoTimeout(scroller);
+ let target_snap_position = inner_snap_area.offsetTop +
+ inner_snap_area.offsetHeight;
+ // Scroll to an offset close to the bottom of the inner snap area and
+ // expect to snap to an offset just below this snap area.
+ scroller.scrollTo(0, target_snap_position - 10);
+ await scrollend_promise;
+ assert_equals(scroller.scrollTop, target_snap_position,
+ "scroller snaps to just below the inner snap area.");
+ // We are just below the inner snap area. Increase its height so that it
+ // is larger than the snapport and straddled by the snapport. Verify
+ // that we snap to its bottom.
+ inner_snap_area.style.height =
+ `${scroller.clientHeight + inner_snap_area.clientHeight - 10}px`;
+ await waitForCompositorCommit();
+ target_snap_position = inner_snap_area.offsetTop +
+ inner_snap_area.offsetHeight - scroller.clientHeight;
+ assert_equals(scroller.scrollTop, target_snap_position,
+ "scroller snaps to the bottom of the smaller snap area (which is now " +
+ "covering).");
+ }, "newly larger-than-snapport area is snapped to when straddled close " +
+ "to bottom.");
+
+ promise_test(async (t) => {
+ await reset();
+ await resetTargetScrollState(t, scroller);
+ assert_equals(scroller.scrollTop, 0, "test precondition: scroller is " +
+ "not scrolled");
+ let scrollend_promise = waitForScrollendEventNoTimeout(scroller);
+ let target_snap_position = inner_snap_area.offsetTop +
+ inner_snap_area.offsetHeight;
+ // Scroll to an offset close to the bottom of the inner snap area and
+ // expect to snap to an offset just below this snap area.
+ scroller.scrollTo(0, target_snap_position - 10);
+ await scrollend_promise;
+ assert_equals(scroller.scrollTop, target_snap_position,
+ "scroller snaps to just below the inner snap area.");
+ // We are just below the inner snap area. Increase its height so that it
+ // is larger than the snapport and covers the snapport. Verify that we
+ // remain in the covering position.
+ inner_snap_area.style.height =
+ `${scroller.clientHeight + inner_snap_area.clientHeight + 10}px`;
+ await waitForCompositorCommit();
+ assert_equals(scroller.scrollTop, target_snap_position,
+ "scroller snaps to the bottom of the smaller snap area (which is now " +
+ "covering).");
+ }, "snapport remains within newly covering snap area when already in " +
+ "covering position.");
+ </script>
+</body> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001-ref.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001-ref.html
new file mode 100644
index 0000000000..88f028fb39
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001-ref.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<title>CSS Scroll Snap Reference</title>
+<style>
+html, body { margin: 0; padding: 0; }
+
+:root {
+ overflow: hidden; /* hide scrollbars for reftest analysis */
+}
+
+#target {
+ position: absolute;
+ top: 25%;
+ width: 100%;
+ margin: 25vh 0;
+ border-top: solid blue;
+}
+</style>
+
+<div id="target">
+ <div>Test passes if the blue line above is centered in the viewport.</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001.html
new file mode 100644
index 0000000000..43028cb874
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-001.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<title>scroll-snap-type + scroll-padding propagates root to viewport</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type'>
+<link rel='help' href='https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding'>
+<link rel='match' href='scroll-snap-root-001-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll snap properties on root are applied to viewport.">
+
+<style type='text/css'>
+html, body { margin: 0; padding: 0; }
+
+:root {
+ scroll-snap-type: block mandatory;
+ scroll-padding: 25%;
+ overflow: hidden; /* hide scrollbars for reftest analysis */
+}
+
+#fail {
+ font: bold 2em;
+ background: red;
+ height: 120vh;
+ margin-bottom: 60vh;
+}
+
+#target {
+ margin-bottom: 120vh;
+ scroll-margin: 25vh;
+ scroll-snap-align: start;
+ border-top: solid blue;
+}
+</style>
+
+<div id="fail">FAIL</div>
+
+<div id="target">
+ <div>Test passes if the blue line above is centered in the viewport.</div>
+</div>
+
+<script>
+ document.getElementById('target').scrollIntoView();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002-ref.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002-ref.html
new file mode 100644
index 0000000000..663b02b8c4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002-ref.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<title>CSS Scroll Snap Reference</title>
+
+<style type='text/css'>
+html, body { margin: 0; padding: 0; }
+
+#target {
+ border-bottom: solid orange thick;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+}
+</style>
+
+<div id="target">
+ <div>Test passes if the orange stripe below is exactly at the bottom of the viewport.</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002.html
new file mode 100644
index 0000000000..302c756341
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-002.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<title>scroll-padding does not propagate body to viewport</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type'>
+<link rel='help' href='https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding'>
+<link rel='match' href='scroll-snap-root-002-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll-snap-padding on body is not applied to viewport.">
+
+<style type='text/css'>
+html, body { margin: 0; padding: 0; }
+
+:root {
+ scroll-snap-type: block mandatory;
+ overflow: hidden; /* hide scrollbars for reftest analysis */
+}
+
+body {
+ scroll-padding: 25%;
+}
+
+#fail {
+ height: 120vh;
+ font: bold 2em;
+ background: red;
+}
+
+#target {
+ margin: 120vh 0;
+ scroll-snap-align: end;
+ border-bottom: solid orange thick;
+}
+</style>
+
+<div id="fail">FAIL</div>
+
+<div id="target">
+ <div>Test passes if the orange stripe below is exactly at the bottom of the viewport.</div>
+</div>
+
+<script>
+ document.getElementById('target').scrollIntoView();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-003.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-003.html
new file mode 100644
index 0000000000..fc7b28fdf5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-root-003.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+<title>scroll-snap-type does not propagate body to viewport</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type'>
+<link rel='help' href='https://drafts.csswg.org/css-scroll-snap-1/#scroll-padding'>
+<link rel='match' href='no-red-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll-snap-type on body is not applied to viewport.">
+
+<style type='text/css'>
+:root {
+ overflow: hidden; /* hide scrollbars for reftest analysis */
+}
+
+body {
+ scroll-snap-type: block mandatory;
+}
+
+#pass {
+ height: 120vh;
+}
+
+#target {
+ scroll-snap-align: start;
+ height: 100vh;
+ background: red;
+}
+</style>
+
+<p id="pass">Test passes if there is no red.
+
+<div id="target">
+ <div>FAIL</div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-001.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-001.html
new file mode 100644
index 0000000000..7d2a228688
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-001.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-snap-stop" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+}
+#scroller {
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+}
+#space {
+ left: 0px;
+ top: 0px;
+ width: 2100px;
+ height: 2100px;
+}
+.target {
+ width: 50px;
+ height: 50px;
+ scroll-snap-align: start;
+}
+.origin {
+ left: 0px;
+ top: 0px;
+}
+.always-stop {
+ left: 100px;
+ top: 0px;
+ scroll-snap-stop: always;
+}
+.closer {
+ left: 200px;
+ top: 0px;
+}
+</style>
+
+<div id="scroller">
+ <div id="space"></div>
+ <div class="target origin"></div>
+ <div class="target always-stop"></div>
+ <div class="target closer"></div>
+</div>
+
+<script>
+var scroller = document.getElementById("scroller");
+test(() => {
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.scrollBy(300, 0);
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 0);
+}, "A scroll with intended direction and end position should not pass a snap " +
+ "area with scroll-snap-stop: always.")
+
+test(() => {
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.scrollTo(300, 0);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 0);
+}, "A scroll with intended end position should always choose the closest snap " +
+ "position regardless of the scroll-snap-stop value.")
+
+// Tests for programmatic scrolls beyond the scroller bounds.
+
+test(() => {
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.scrollBy(100000, 0);
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 0);
+}, "A scroll outside bounds in the snapping axis with intended direction and " +
+ "end position should not pass a snap area with scroll-snap-stop: always.")
+
+test(() => {
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.scrollBy(300, -10);
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 0);
+}, "A scroll outside bounds in the non-snapping axis with intended direction " +
+ "and end position should not pass a snap area with scroll-snap-stop: always.")
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002-nested.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002-nested.tentative.html
new file mode 100644
index 0000000000..34b6dc97e2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002-nested.tentative.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-snap-stop" />
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+}
+.scroller {
+ width: 400px;
+ height: 100px;
+ overflow: scroll;
+ scroll-snap-type: x mandatory;
+}
+#space {
+ left: 0px;
+ top: 0px;
+ width: 2100px;
+ height: 50px;
+}
+.target {
+ width: 50px;
+ height: 50px;
+ scroll-snap-align: start;
+ top: 0px;
+}
+</style>
+
+<div class="scroller" id="scroller7">
+ <div id="space"></div>
+ <div class="target" style="left: 0px;"></div>
+ <div class="target" style="left: 400px; width: 900px;"></div>
+ <div class="target" style="left: 900px; scroll-snap-stop: always;"></div
+</div>
+
+<script>
+
+test(() => {
+ assert_equals(scroller7.scrollLeft, 0);
+ assert_equals(scroller7.scrollTop, 0);
+
+ // start dest always
+ // |-------------------------|================|=====|=====
+ // 0 400 700 900
+
+ // Scoll on the element whose snap area is larger than the snapport.
+ scroller7.scrollBy(600, 0);
+ // Stops before the smaller snap area @ 900px enters the snapport.
+ assert_equals(scroller7.scrollLeft + scroller7.clientWidth, 900);
+ assert_equals(scroller7.scrollTop, 0);
+
+ scroller7.scrollTo(0, 0);
+ scroller7.scrollBy(800, 0);
+ // Now the smaller snap area is closer, so we snap to it.
+ assert_equals(scroller7.scrollLeft, 900);
+ assert_equals(scroller7.scrollTop, 0);
+}, "`scroll-snap-stop: always` snap point is further than the scroll " +
+ "destination and a snap area covers the snapport");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002.html
new file mode 100644
index 0000000000..1aae7aea60
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-002.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-snap-stop" />
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-overflow" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+}
+.scroller {
+ width: 400px;
+ height: 100px;
+ overflow: scroll;
+ scroll-snap-type: x mandatory;
+}
+#space {
+ left: 0px;
+ top: 0px;
+ width: 2100px;
+ height: 50px;
+}
+.target {
+ width: 50px;
+ height: 50px;
+ scroll-snap-align: start;
+ top: 0px;
+}
+</style>
+
+<!--
+ start dest closest always
+ |------------------------------|--------|--------|
+ -->
+<div class="scroller" id="scroller1">
+ <div id="space"></div>
+ <div class="target" style="left: 0px;"></div>
+ <!-- Add `scroll-snap-stop: always` element into the DOM tree prior to the
+ closest snap point -->
+ <div class="target" style="left: 120px; scroll-snap-stop: always;"></div>
+ <!-- Add a snap point closest-to-destination but further than the destination
+ from the start position -->
+ <div class="target" style="left: 110px;"></div>
+</div>
+
+<!--
+ start closest dest always
+ |------------------------------|------|--------|
+ -->
+<div class="scroller" id="scroller2">
+ <div id="space"></div>
+ <div class="target" style="left: 0px;"></div>
+ <div class="target" style="left: 120px; scroll-snap-stop: always;"></div>
+ <!-- Add a snap point closest-to-destination and closer than the destination
+ from the start position -->
+ <div class="target" style="left: 95px;"></div>
+</div>
+</div>
+
+<!--
+ A test case where there's a snap point whose snap area covers the snapport and
+ there's a `scroll-snap-stop: always` snap point on the opposite side.
+ -->
+<div class="scroller" id="scroller3">
+ <div id="space"></div>
+ <div class="target" style="left: 0px;"></div>
+ <div class="target" style="left: 700px; scroll-snap-stop: always;"></div>
+ <!-- Add a snap point where the snap area covers the snapport entirely -->
+ <div class="target" style="left: 100px; width: 500px;"></div>
+ <!-- Add a snap point where the distance between this and the 100px point is
+ larger than the snapport size (400px) -->
+ <div class="target" style="left: 600px;"></div>
+</div>
+
+<!--
+ A similar case above, but two `scroll-snap-stop: always` snap points.
+ -->
+<div class="scroller" id="scroller4">
+ <div id="space"></div>
+ <div class="target" style="left: 0px;"></div>
+ <div class="target" style="left: 700px; scroll-snap-stop: always;"></div>
+ <!-- Add a snap point where the snap area covers the snapport entirely -->
+ <div class="target" style="left: 100px; width: 500px;"></div>
+ <!-- Add a snap point where the distance between this and the 100px point is
+ larger than the snapport size (400px) -->
+ <div class="target" style="left: 600px; scroll-snap-stop: always;"></div>
+</div>
+
+<!--
+ Another similar case, but the nearest snap point to the start point is
+ `scroll-snap-stop: always`.
+ -->
+<div class="scroller" id="scroller5">
+ <div id="space"></div>
+ <div class="target" style="left: 0px;"></div>
+ <div class="target" style="left: 700px; scroll-snap-stop: always;"></div>
+ <!-- Add a snap point where the snap area covers the snapport entirely -->
+ <div class="target" style="left: 100px; width: 500px; scroll-snap-stop: always;"></div>
+ <!-- Add a snap point where the distance between this and the 100px point is
+ larger than the snapport size (400px) -->
+ <div class="target" style="left: 600px;"></div>
+</div>
+
+<!--
+ A test case that a `scroll-snap-stop: always` snap point is further than the
+ scroll destination.
+ -->
+<div class="scroller" id="scroller6">
+ <div id="space"></div>
+ <div class="target" style="left: 0px;"></div>
+ <div class="target" style="left: 400px;"></div>
+ <div class="target" style="left: 700px; scroll-snap-stop: always;"></div
+</div>
+
+<script>
+
+test(() => {
+ assert_equals(scroller1.scrollLeft, 0);
+ assert_equals(scroller1.scrollTop, 0);
+
+ // start dest closest always
+ // |------------------------------|--------|--------|
+ // 0 100 110 120
+ scroller1.scrollBy(100, 0);
+ assert_equals(scroller1.scrollLeft, 110);
+ assert_equals(scroller1.scrollTop, 0);
+}, "The closest snap point is preferred than scroll-snap-stop: always where " +
+ "it's further than the destination (the closest one is closer to the " +
+ "scroll start position than the destination)");
+
+test(() => {
+ assert_equals(scroller2.scrollLeft, 0);
+ assert_equals(scroller2.scrollTop, 0);
+
+ // start closest dest always
+ // |------------------------------|------|--------|
+ // 0 95 100 120
+ scroller2.scrollBy(100, 0);
+ assert_equals(scroller2.scrollLeft, 95);
+ assert_equals(scroller2.scrollTop, 0);
+}, "The closest snap point is preferred than scroll-snap-stop: always where " +
+ "it's further than the destination (the closest one is futrher than the " +
+ "destination from the start position)");
+
+test(() => {
+ assert_equals(scroller3.scrollLeft, 0);
+ assert_equals(scroller3.scrollTop, 0);
+
+ // start dest always
+ // |-----|===|============================|------|
+ // 0 100 150 600 700
+
+ // Scoll on the element whose snap area is larger than the snapport.
+ scroller3.scrollBy(150, 0);
+ assert_equals(scroller3.scrollLeft, 150);
+ assert_equals(scroller3.scrollTop, 0);
+}, "The scroll destination on a large element whose snap area covers " +
+ "the snapport entirely is a valid snap position");
+
+test(() => {
+ assert_equals(scroller4.scrollLeft, 0);
+ assert_equals(scroller4.scrollTop, 0);
+
+ // start dest always always
+ // |-----|====|============================|------|
+ // 0 100 150 600 700
+
+ // Scoll on the element whose snap area is larger than the snapport.
+ scroller4.scrollBy(150, 0);
+ assert_equals(scroller4.scrollLeft, 150);
+ assert_equals(scroller4.scrollTop, 0);
+}, "The scroll destination on a large element whose snap area covers " +
+ "the snapport entirely is a valid snap position (with two " +
+ "`scroll-snap-stop: always` snap points");
+
+test(() => {
+ assert_equals(scroller5.scrollLeft, 0);
+ assert_equals(scroller5.scrollTop, 0);
+
+ // start always dest always
+ // |-----|=====|============================|------|
+ // 0 100 150 600 700
+
+ // Scoll on the element whose snap area is larger than the snapport, but
+ // the scroll-snap-stop property is `always`.
+ scroller5.scrollBy(150, 0);
+ assert_equals(scroller5.scrollLeft, 100);
+ assert_equals(scroller5.scrollTop, 0);
+
+ // Scroll the direction further, it should NOT be trapped by the
+ // `scroll-snap-stop: always` snap point.
+ scroller5.scrollBy(50, 0);
+ assert_equals(scroller5.scrollLeft, 150);
+ assert_equals(scroller5.scrollTop, 0);
+}, "`scroll-snap-stop: always` snap point is preferred even if the snap area " +
+ "entire snapport");
+
+test(() => {
+ assert_equals(scroller6.scrollLeft, 0);
+ assert_equals(scroller6.scrollTop, 0);
+
+ // start dest always
+ // |-------------------------|-----------------|------|
+ // 0 400 600 700
+
+ // Scroll to a point where it's closer to a `scroll-snap-stop: always` snap
+ // position.
+ scroller6.scrollBy(600, 0);
+ assert_equals(scroller6.scrollLeft, 700);
+ assert_equals(scroller6.scrollTop, 0);
+}, "`scroll-snap-stop: always` snap point is further than the scroll " +
+ "destination");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-change.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-change.html
new file mode 100644
index 0000000000..4615487f56
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-change.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-snap-stop" />
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div, html, body {
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ scroll-snap-type: x mandatory;
+ overflow: scroll;
+}
+
+#scroller {
+ scroll-snap-type: x mandatory;
+ overflow: scroll;
+ height: 400px;
+ width: 400px;
+}
+
+.large_space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap_area {
+ scroll-snap-align: none start;
+ width: 100px;
+ height: 100px;
+
+ background-color: blue;
+}
+
+.snap_area:nth-child(1) {
+ margin-left: 0;
+}
+
+.snap_area:nth-child(2) {
+ margin-left: 100px;
+}
+
+.snap_area:nth-child(3) {
+ margin-left: 300px;
+}
+
+.snap_area:nth-child(4) {
+ margin-left: 500px;
+}
+</style>
+
+<!-- Add snap area to the root scroller -->
+<div class="snap_area"></div>
+<div class="snap_area"></div>
+<div class="snap_area"></div>
+<div class="snap_area"></div>
+
+<div id="scroller">
+ <div class="snap_area"></div>
+ <div class="snap_area"></div>
+ <div class="snap_area"></div>
+ <div class="snap_area"></div>
+ <div class="large_space"></div>
+</div>
+
+<div class="large_space"></div>
+
+<script>
+const scrollers = [document.scrollingElement, document.getElementById("scroller")];
+for (const scroller of scrollers) {
+ test(_ => {
+ assert_equals(scroller.scrollLeft, 0); // sanity check
+
+ scroller.querySelectorAll('.snap_area').forEach(area => area.style.scrollSnapStop = 'always');
+ scroller.scrollBy(500, 0);
+ assert_equals(scroller.scrollLeft, 100, "scrollBy should not skip area with stop always");
+
+ scroller.querySelectorAll('.snap_area').forEach(area => area.style.scrollSnapStop = 'normal');
+ scroller.scrollBy(500, 0);
+ assert_equals(scroller.scrollLeft, 500, "scrollBy should skip secon area with stop normal");
+
+ // When snap type is changed back to mandatory, scrolling should snap.
+ scroller.querySelectorAll('.snap_area').forEach(area => area.style.scrollSnapStop = 'always');
+ scroller.scrollBy(-500, 0);
+ assert_equals(scroller.scrollLeft, 300, "scrollBy should not skip area with stop always");
+ }, `scroll-snap-stop for areas on ${scroller.nodeName} should control snapping behavior and changing it takes effect`);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-dynamic-crash.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-dynamic-crash.html
new file mode 100644
index 0000000000..0039a0168e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-stop-dynamic-crash.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1472537">
+<style>
+div { overflow-y: scroll; }
+::-webkit-scrollbar { width: 10px; }
+::-webkit-scrollbar-corner { }
+.crash::-webkit-scrollbar-corner {
+ scroll-snap-stop: always;
+}
+</style>
+<div id="target"></div>
+<script>
+document.body.offsetTop;
+document.getElementById('target').className = 'crash';
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-change.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-change.html
new file mode 100644
index 0000000000..ed86bace44
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-change.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-snap-type" />
+<meta name="viewport" content="user-scalable=no">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div, html, body {
+ margin: 0;
+ padding: 0;
+}
+html {
+ margin: 0px;
+ overflow: scroll;
+}
+#scroller {
+ margin-left: 200px;
+
+ height: 300px;
+ width: 300px;
+ overflow: scroll;
+}
+.space {
+ width: 2000px;
+ height: 2000px;
+}
+.snap_area {
+ margin-left: 200px;
+ top: 0;
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: none start;
+}
+</style>
+
+<!-- Add snap area to the root scroller -->
+<div class="snap_area" id="viewport"></div>
+
+<div id="scroller">
+ <div class="snap_area" id="inner"></div>
+ <div class="space"></div>
+</div>
+
+<div class="space"></div>
+
+<script>
+
+const scrollers = [document.scrollingElement, document.getElementById("scroller")];
+for (const scroller of scrollers) {
+ test(_ => {
+ scroller.style.scrollSnapType = 'x mandatory';
+ scroller.scrollTo(100, 0);
+ assert_equals(scroller.scrollLeft, 200, "scrolling should snap");
+
+ // When snap type is 'none' the scroller, scrolling should not snap.
+ scroller.style.scrollSnapType = 'none';
+ scroller.scrollTo(100, 0);
+ assert_equals(scroller.scrollLeft, 100, "scrolling should not snap");
+
+ // When snap type is changed back to mandatory, scrolling should snap.
+ scroller.style.scrollSnapType = 'x mandatory';
+ scroller.scrollTo(110, 0);
+ assert_equals(scroller.scrollLeft, 200, "scrolling should snap after change");
+ }, `scroll-snap-type on ${scroller.nodeName} should control snapping behavior and changing it takes effect`);
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-on-root-element.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-on-root-element.html
new file mode 100644
index 0000000000..a45ac92e5a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type-on-root-element.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<link rel="help" href="https://drafts.csswg.org/css-writing-modes-4/#principal-flow" />
+<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<style>
+body {
+ height: 8000px;
+ width: 3000px;
+ margin: 0px;
+}
+#target {
+ position: absolute;
+ background-color: blue;
+ top: 2000px;
+ left: 100px;
+
+ width: 100vw;
+ height: 100px;
+}
+</style>
+<div id="target"></div>
+<script>
+const documentHeight = document.documentElement.clientHeight;
+
+function cleanup() {
+ document.documentElement.style.scrollSnapType = "none";
+ target.style.scrollSnapAlign = "";
+ document.body.style.writingMode = "";
+ window.scrollTo(0, 0);
+}
+
+test(t => {
+ t.add_cleanup(cleanup);
+ document.documentElement.style.scrollSnapType = "y mandatory";
+ target.style.scrollSnapAlign = "end none";
+
+ window.scrollTo(0, 1800);
+
+ // `target y (2000px)` + `target height (100px)` - document height.
+ assert_equals(document.scrollingElement.scrollTop, 2100 - documentHeight);
+ assert_equals(document.scrollingElement.scrollLeft, 0, "x should not snap");
+}, "The scroll-snap-type on the root element is applied");
+
+test(t => {
+ t.add_cleanup(cleanup);
+
+ document.documentElement.style.scrollSnapType = "inline mandatory";
+ document.body.style.writingMode = "vertical-lr";
+ target.style.scrollSnapAlign = "none end";
+
+ window.scrollTo(200, 1800);
+
+ // Since inline axis is vertical, scrolling viewport vertically on block
+ // axis should snap.
+ assert_equals(document.scrollingElement.scrollTop, 2100 - documentHeight, "inline should snap");
+ // `target x (100px)`.
+ assert_equals(document.scrollingElement.scrollLeft, 200, "block should not snap");
+}, "The writing-mode (vertical-lr) on the body is used");
+
+test(t => {
+ t.add_cleanup(cleanup);
+
+ document.documentElement.style.scrollSnapType = "inline mandatory";
+ document.body.style.writingMode = "horizontal-tb"; // inline is horizontal
+ target.style.scrollSnapAlign = "none start";
+
+ window.scrollTo(200, 1800);
+
+ // Though the target's width is 100vw, there may be room to scroll the window
+ // within the target because of the scrollbar width and the browser may snap
+ // to the right edge (instead of the left edge of the target, since it is
+ // closer to the offset of 200 and still a valid snap point) within this room.
+ const scrollbar_width = window.innerWidth - document.documentElement.clientWidth;
+ assert_approx_equals(document.scrollingElement.scrollLeft, 100, scrollbar_width, "inline should snap");
+ assert_equals(document.scrollingElement.scrollTop, 1800, "block should not snap");
+}, "The writing-mode (horizontal-tb) on the body is used ");
+</script>
+</body>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type.html
new file mode 100644
index 0000000000..1577aa7afc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-snap-type.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-snap-type" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+#scroller {
+ height: 400px;
+ width: 400px;
+ overflow: scroll;
+}
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap {
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+#left-top {
+ left: 0px;
+ top: 0px;
+}
+#right-bottom {
+ left: 1000px;
+ top: 1000px;
+}
+</style>
+
+<div id="scroller">
+ <div id="space"></div>
+ <div class="snap" id="left-top"></div>
+ <div class="snap" id="right-bottom"></div>
+</div>
+
+<script>
+var scroller = document.getElementById("scroller");
+var visible_x = 1000 - scroller.clientWidth;
+var visible_y = 1000 - scroller.clientHeight;
+
+test(() => {
+ scroller.style.scrollSnapType = "both mandatory";
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.scrollTo(visible_x + 10, visible_y + 10);
+ assert_equals(scroller.scrollLeft, 1000);
+ assert_equals(scroller.scrollTop, 1000);
+}, "mandatory scroll-snap-type should snap as long as the element is visible.");
+
+test(() => {
+ scroller.style.scrollSnapType = "both proximity";
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.scrollTo(visible_x + 10, visible_y + 10);
+ assert_equals(scroller.scrollLeft, visible_x + 10);
+ assert_equals(scroller.scrollTop, visible_y + 10);
+}, "proximity scroll-snap-type shouldn't snap if the snap position is too far away.");
+
+test(() => {
+ scroller.style.scrollSnapType = "both proximity";
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.scrollTo(995, 995);
+ assert_equals(scroller.scrollLeft, 1000);
+ assert_equals(scroller.scrollTop, 1000);
+}, "proximity scroll-snap-type should snap if the snap position is close.");
+
+test(_ => {
+ scroller.style.scrollSnapType = "none";
+ scroller.scrollTo(100, 100);
+ assert_equals(scroller.scrollLeft, 100, "scrolling should not snap");
+ assert_equals(scroller.scrollTop, 100, "scrolling should not snap");
+}, "none scroll-snap-type shouldn't snap.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-001-ref.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-001-ref.html
new file mode 100644
index 0000000000..28b00184c2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-001-ref.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<title>Reference</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+ font: 20px/1 sans-serif;
+ }
+ .container > div {
+ height: 1em;
+ margin: 1em 0;
+ background: green;
+ }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-001.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-001.html
new file mode 100644
index 0000000000..eeda674e07
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-001.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<title>#target and snap position with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#choosing'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name="flags" content="may">
+<meta name='assert'
+ content="Test passes if scroll snapping is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when navigating to an element with the target fragment ID.">
+
+<style type='text/css'>
+ iframe {
+ border: solid blue 4px;
+ height: 80px;
+ width: calc(100% - 8px);
+ }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<iframe class="container" src="support/scroll-target-align-001-iframe.html#target">This UA doesn't support iframes; please request a custom version of this test!</iframe>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-002.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-002.html
new file mode 100644
index 0000000000..01db026dff
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-002.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html>
+<title>scrollIntoView() and snap position with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#choosing'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name="flags" content="may">
+<meta name='assert'
+ content="Test passes if scroll snapping is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when scrolling an element into view
+ explicitly by script.">
+
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div {
+ height: 1em;
+ }
+ .container { scroll-padding: .5em 0 0; } /* set up a snap position */
+ #target { scroll-margin: .5em 0 0;
+ scroll-snap-align: center; }
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <div id="target"></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').scrollIntoView();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-003.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-003.html
new file mode 100644
index 0000000000..d13efa0abb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-align-003.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<title>focus() and snap position with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#choosing'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name="flags" content="may">
+<meta name='assert'
+ content="Test passes if scroll snapping is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when scrolling an element into view
+ even if that operation is implied (in this case, by .focus()).">
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div, a {
+ height: 1em;
+ display: block;
+ outline: none;
+ }
+ .container { scroll-padding: .5em 0 0; } /* set up a snap position */
+ #target { scroll-margin: .5em 0 0;
+ scroll-snap-align: center; }
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <a href="" id="target"></a>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').focus();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-001.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-001.html
new file mode 100644
index 0000000000..8ddbbcec5f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-001.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html>
+<title>#target and scroll-margin with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-margin'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name="flags" content="should">
+<meta name='assert'
+ content="Test passes if scroll-margin is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when navigating to an element with the target fragment ID.">
+
+<style type='text/css'>
+ iframe {
+ border: solid blue 4px;
+ height: 80px;
+ width: calc(100% - 8px);
+ }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<iframe class="container" src="support/scroll-target-margin-001-iframe.html#target">This UA doesn't support iframes; please request a custom version of this test!</iframe>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-002.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-002.html
new file mode 100644
index 0000000000..51cf553b36
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-002.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<title>scrollIntoView() and scroll-margin with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-margin'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name="flags" content="should">
+<meta name='assert'
+ content="Test passes if scroll-margin is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when scrolling an element into view
+ explicitly by script.">
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div {
+ height: 1em;
+ }
+ #target { scroll-margin: 2em 0 1em; } /* snap area is exact fit for snapport */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <div id="target"></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').scrollIntoView();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-003.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-003.html
new file mode 100644
index 0000000000..2ea8eff67c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-003.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<title>focus() and scroll-margin with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-margin'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name="flags" content="should">
+<meta name='assert'
+ content="Test passes if scroll-margin is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when scrolling an element into view
+ even if that operation is implied (in this case, by .focus()).">
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div, a {
+ height: 1em;
+ display: block;
+ outline: none;
+ }
+ #target { scroll-margin: 2em 0 1em; } /* snap area is exact fit for snapport */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <a href="" id="target"></a>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').focus();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-004.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-004.html
new file mode 100644
index 0000000000..ac87c8684d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-004.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html>
+<title>scroll-margin on elements with 'display: table;'</title>
+<link rel='author' title='Jan Grosser' href='mailto:jan_firefoxdev@t-online.de'>
+<link rel='help' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1633192'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-margin'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name="flags" content="should">
+<meta name='assert' content="Test passes if scroll-margin is honored on elements with 'display: table'.">
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div {
+ height: 1em;
+ }
+ #target { scroll-margin: 2em 0 1em; display: table; } /* snap area is exact fit for snapport */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <div id="target"></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').scrollIntoView();
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-005.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-005.html
new file mode 100644
index 0000000000..9296202354
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-005.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<title>scroll-margin on input widget</title>
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<link rel="help" href="https://www.w3.org/TR/css-scroll-snap-1/#scroll-margin">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1729292">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #target {
+ scroll-margin-top: 200px;
+ }
+ .padding {
+ height: 5000px;
+ }
+</style>
+
+<div id="container">
+ <div class="padding"></div>
+ <input type="date" id="target">
+ <div class="padding"></div>
+</div>
+
+<script>
+promise_test(async function() {
+ document.scrollingElement.scrollTo(0, 20000);
+ await new Promise(resolve => {
+ document.addEventListener("scroll", resolve, { once: true });
+ document.getElementById("target").focus();
+ });
+ // Should be around 5000 - 200px of margin - (window.innerHeight / 2)
+ const targetPos = 4900 - (window.innerHeight / 2);
+ assert_between_exclusive(document.scrollingElement.scrollTop, targetPos - 300,
+ targetPos + 300, "Should honor date input scroll-margin");
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-006.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-006.html
new file mode 100644
index 0000000000..48e246249f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-margin-006.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<title>scrollIntoView() and scroll-margin applied to an inline element</title>
+<link rel='author' title='Martin Robinson' href='http://igalia.com'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-margin'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style type='text/css'>
+ .container {
+ border: solid black 1px;
+ height: 40px;
+ width: 40px;
+ overflow: auto;
+ }
+</style>
+
+<div class="container">
+ <div style="height: 1000px; width: 2000px;"></div>
+ <div style="width: 2000px;">
+ <span>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</span>
+ <span id="target">TARGETTARGETTARGETTARGET</span>
+ <span>XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</span>
+ </div>
+ <div style="height: 1000px; width: 2000px;"></div>
+</div>
+
+<script>
+
+test(() => {
+ target.scrollIntoView();
+ const scroller = target.parentElement.parentElement;
+ let expectedScrollPosition = [scroller.scrollLeft - 20, scroller.scrollTop - 20];
+ scroller.scrollTo(0, 0);
+
+ target.style.scrollMarginTop = "20px";
+ target.style.scrollMarginLeft = "20px";
+ target.scrollIntoView();
+ assert_equals(scroller.scrollLeft, expectedScrollPosition[0]);
+ assert_equals(scroller.scrollTop, expectedScrollPosition[1]);
+
+ target.style.scrollMarginTop = "0px";
+ target.style.scrollMarginLeft = "0px";
+
+ scroller.scrollTo(2000, 2000);
+ target.scrollIntoView({"block": "end", "inline": "end"});
+ expectedScrollPosition = [scroller.scrollLeft + 20, scroller.scrollTop + 20];
+ scroller.scrollTo(2000, 2000);
+
+ target.style.scrollMarginBottom = "20px";
+ target.style.scrollMarginRight = "20px";
+ target.scrollIntoView({"block": "end", "inline": "end"});
+ assert_equals(scroller.scrollLeft, expectedScrollPosition[0]);
+ assert_equals(scroller.scrollTop, expectedScrollPosition[1]);
+
+}, "scroll-margin is taken into account when scrolling an inline element into view");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-001.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-001.html
new file mode 100644
index 0000000000..5cd4fddcc5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-001.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<title>#target and scroll-padding with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-padding'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll-padding is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when navigating to an element with the target fragment ID.">
+<style type='text/css'>
+ iframe {
+ border: solid blue 4px;
+ height: 80px;
+ width: calc(100% - 8px);
+ }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<iframe class="container" src="support/scroll-target-padding-001-iframe.html#target">This UA doesn't support iframes; please request a custom version of this test!</iframe>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-002.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-002.html
new file mode 100644
index 0000000000..fbed1e132e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-002.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html>
+<title>scrollIntoView() and scroll-padding with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-padding'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll-padding is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when scrolling an element into view
+ explicitly by script.">
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div {
+ height: 1em;
+ }
+ .container { scroll-padding: 2em 0 1em; } /* snap area is exact fit for snapport */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <div id="target"></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').scrollIntoView();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-003.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-003.html
new file mode 100644
index 0000000000..ccbe7b0ec9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-padding-003.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<title>focus() and scroll-padding with snapping off (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#scroll-margin'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll-padding is honored
+ on a scroll container with 'scroll-snap-type: none'
+ when scrolling an element into view
+ even if that operation is implied (in this case, by .focus()).">
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div, a {
+ height: 1em;
+ display: block;
+ outline: none;
+ }
+ .container { scroll-padding: 2em 0 1em; } /* snap area is exact fit for snapport */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <a href="" id="target"></a>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').focus();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-001.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-001.html
new file mode 100644
index 0000000000..76d3222a0b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-001.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<title>#target and snap position with snapping on (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#choosing'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll snapping is honored
+ when navigating to an element with the target fragment ID.">
+
+<style type='text/css'>
+ iframe {
+ border: solid blue 4px;
+ height: 80px;
+ width: calc(100% - 8px);
+ }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<iframe class="container" src="support/scroll-target-snap-001-iframe.html#target">This UA doesn't support iframes; please request a custom version of this test!</iframe>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-002.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-002.html
new file mode 100644
index 0000000000..6e928a435e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-002.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+<title>scrollIntoView() and snap position with snapping on (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#choosing'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll snapping is honored
+ when scrolling an element into view
+ explicitly by script.">
+
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+ scroll-snap-type: block;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div {
+ height: 1em;
+ }
+ /* Note: we use "start" for #target to avoid spec ambiguity where
+ * scroll-snap-align conflicts with default ScrollIntoViewOptions.
+ * See https://github.com/w3c/csswg-drafts/issues/9576.
+ */
+ #target { scroll-margin: 2em 0 0;
+ scroll-snap-align: start; } /* set up a snap position */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* Try to foil the UA */
+ .foilup { margin-bottom: -1em; scroll-snap-align: start; }
+ .foildn { margin-top: -1em; scroll-snap-align: end; }
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="foilup"></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <div class="foilup"></div>
+ <div id="target"></div>
+ <div class="foildn"></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div class="foildn"></div>
+ <div></div>
+ <div class="foildn"></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').scrollIntoView();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-003.html b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-003.html
new file mode 100644
index 0000000000..6fe3901e51
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scroll-target-snap-003.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+<title>focus() and snap position with snapping on (y-axis)</title>
+<link rel='author' title='Elika J. Etemad' href='http://fantasai.inkedblade.net/contact'>
+<link rel='help' href='https://www.w3.org/TR/css-scroll-snap-1/#choosing'>
+<link rel='match' href='scroll-target-001-ref.html'>
+<meta name='assert'
+ content="Test passes if scroll snapping is honored
+ when scrolling an element into view
+ even if that operation is implied (in this case, by .focus()).">
+
+<style type='text/css'>
+ .container {
+ border: solid blue 4px;
+ height: 4em;
+ overflow: auto;
+ scroll-snap-type: block;
+
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ .container > div, a {
+ height: 1em;
+ display: block;
+ outline: none;
+ }
+ #target { scroll-margin: 1em 0 0;
+ scroll-snap-align: center; } /* set up a snap position */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* Try to foil the UA */
+ .foilup { margin-bottom: -1em; scroll-snap-align: start; }
+ .foildn { margin-top: -1em; scroll-snap-align: end; }
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div id='instructions'>Test passes if there is a green stripe across the second quarter of the box below and no red.</div>
+
+<div class="container">
+ <div></div>
+ <div></div>
+ <div></div>
+ <div></div>
+ <div class="foilup"></div>
+ <div class="fail">FAIL</div>
+ <div></div>
+ <div id="stripe"></div>
+ <div class="foilup"></div>
+ <a href="" id="target"></a>
+ <div class="foildn"></div>
+ <div></div>
+ <div class="fail">FAIL</div>
+ <div class="foildn"></div>
+ <div></div>
+ <div class="foildn"></div>
+ <div></div>
+ <div></div>
+ <div></div>
+</div>
+
+<script>
+ document.getElementById('target').focus();
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/scrollTo-scrollBy-snaps.html b/testing/web-platform/tests/css/css-scroll-snap/scrollTo-scrollBy-snaps.html
new file mode 100644
index 0000000000..6013de5044
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/scrollTo-scrollBy-snaps.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<meta name="viewport" content="user-scalable=no">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+html {
+ margin: 0px;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+}
+div {
+ position: absolute;
+}
+.scroller {
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+}
+#inner-scroller {
+ top: 3000px;
+ width: 800px;
+ height: 800px;
+}
+.space {
+ left: 0px;
+ top: 0px;
+ width: 4000px;
+ height: 4000px;
+}
+.target {
+ width: 600px;
+ height: 600px;
+ scroll-snap-align: start;
+}
+
+.left {
+ left: 0px;
+}
+.right {
+ left: 1000px;
+}
+.top {
+ top: 0px;
+}
+.bottom {
+ top: 1000px;
+}
+</style>
+<body class="scroller">
+ <div class="space"></div>
+ <div class="target left top"></div>
+ <div class="target right top"></div>
+ <div class="target left bottom"></div>
+ <div class="target right bottom"></div>
+ <div class="scroller" id="inner-scroller">
+ <div class="space"></div>
+ <div class="target left top"></div>
+ <div class="target right top"></div>
+ <div class="target left bottom"></div>
+ <div class="target right bottom"></div>
+ </div>
+</body>
+
+<script>
+function format_dict(dict) {
+ const props = [];
+ for (let prop in dict) {
+ props.push(`${prop}: ${format_value(dict[prop])}`);
+ }
+ return `{${props.join(', ')}}`;
+}
+
+var divScroller = document.getElementById("inner-scroller");
+var viewport = document.scrollingElement;
+[
+ [{left: 800}, 1000, 0],
+ [{top: 900}, 0, 1000],
+ [{left: 900, top: 800}, 1000, 1000],
+ [{left: 800, top: -100}, 1000, 0], /* outside bounds on y axis */
+ [{left: 10000, top: -100}, 1000, 0] /* outside bounds on both axes */
+].forEach(([input, expectedX, expectedY]) => {
+ test(() => {
+ divScroller.scrollTo(0, 0);
+ assert_equals(divScroller.scrollLeft, 0);
+ assert_equals(divScroller.scrollTop, 0);
+ if (input.left)
+ divScroller.scrollLeft = input.left;
+ if (input.top)
+ divScroller.scrollTop = input.top;
+ assert_equals(divScroller.scrollLeft, expectedX);
+ assert_equals(divScroller.scrollTop, expectedY);
+ }, `assign scrollLeft and scrollTop for ${format_dict(input)} on div lands on (${expectedX}, ${expectedY})`);
+
+ test(() => {
+ viewport.scrollTo(0, 0);
+ assert_equals(viewport.scrollLeft, 0);
+ assert_equals(viewport.scrollTop, 0);
+ if (input.left)
+ viewport.scrollLeft = input.left;
+ if (input.top)
+ viewport.scrollTop = input.top;
+ assert_equals(viewport.scrollLeft, expectedX);
+ assert_equals(viewport.scrollTop, expectedY);
+ }, `assign scrollLeft and scrollTop for ${format_dict(input)} on viewport-defining element lands on (${expectedX}, ${expectedY})`);
+
+ test(() => {
+ divScroller.scrollTo(0, 0);
+ assert_equals(divScroller.scrollLeft, 0);
+ assert_equals(divScroller.scrollTop, 0);
+ divScroller.scrollTo(input);
+ assert_equals(divScroller.scrollLeft, expectedX);
+ assert_equals(divScroller.scrollTop, expectedY);
+ }, `scrollTo(${format_dict(input)}) on div lands on (${expectedX}, ${expectedY})`);
+
+ test(() => {
+ divScroller.scrollTo(0, 0);
+ assert_equals(divScroller.scrollLeft, 0);
+ assert_equals(divScroller.scrollTop, 0);
+ divScroller.scrollBy(input);
+ assert_equals(divScroller.scrollLeft, expectedX);
+ assert_equals(divScroller.scrollTop, expectedY);
+ }, `scrollBy(${format_dict(input)}) on div lands on (${expectedX}, ${expectedY})`);
+
+ test(() => {
+ viewport.scrollTo(0, 0);
+ assert_equals(viewport.scrollLeft, 0);
+ assert_equals(viewport.scrollTop, 0);
+ viewport.scrollTo(input);
+ assert_equals(viewport.scrollLeft, expectedX);
+ assert_equals(viewport.scrollTop, expectedY);
+ }, `scrollTo(${format_dict(input)}) on viewport-defining element lands on (${expectedX}, ${expectedY})`);
+
+ test(() => {
+ viewport.scrollTo(0, 0);
+ assert_equals(viewport.scrollLeft, 0);
+ assert_equals(viewport.scrollTop, 0);
+ viewport.scrollBy(input);
+ assert_equals(viewport.scrollLeft, expectedX);
+ assert_equals(viewport.scrollTop, expectedY);
+ }, `scrollBy(${format_dict(input)}) on viewport-defining element lands on (${expectedX}, ${expectedY})`);
+
+ test(() => {
+ window.scrollTo(0, 0);
+ assert_equals(window.scrollX, 0);
+ assert_equals(window.scrollY, 0);
+ window.scrollTo(input);
+ assert_equals(window.scrollX, expectedX);
+ assert_equals(window.scrollY, expectedY);
+ }, `scrollTo(${format_dict(input)}) on window lands on (${expectedX}, ${expectedY})`);
+
+ test(() => {
+ window.scrollTo(0, 0);
+ assert_equals(window.scrollX, 0);
+ assert_equals(window.scrollY, 0);
+ window.scrollBy(input);
+ assert_equals(window.scrollX, expectedX);
+ assert_equals(window.scrollY, expectedY);
+ }, `scrollBy(${format_dict(input)}) on window lands on (${expectedX}, ${expectedY})`);
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/selection-target.html b/testing/web-platform/tests/css/css-scroll-snap/selection-target.html
new file mode 100644
index 0000000000..1c8435ea6e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/selection-target.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>scroll-padding is respected when typing into an out-of-view textfield</title>
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1701928">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#scroll-padding">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<style>
+ body {
+ margin: 0;
+ }
+ :root {
+ scroll-padding-top: 100px;
+ }
+</style>
+<div style="height: 300vh;"></div>
+<input type="text">
+<div style="height: 300vh;"></div>
+<script>
+function tick() {
+ return new Promise(resolve => {
+ requestAnimationFrame(() => requestAnimationFrame(resolve));
+ });
+}
+
+promise_test(async function() {
+ let input = document.querySelector("input");
+ input.focus();
+ await tick();
+ // Scroll out of view.
+ scrollTo(0, document.scrollingElement.scrollHeight);
+ await tick();
+ assert_not_equals(window.scrollY, 0);
+ assert_true(input.getBoundingClientRect().bottom < 0, "Should be offscreen");
+ // NOTE(emilio): Using test_driver.Actions() instead of test_driver.send_keys
+ // because the later scrolls the target into view which would defeat the
+ // point of the test...
+ await new test_driver.Actions().keyDown('a').send();
+ await tick();
+ // We assert the bottom rather than the top because Gecko scrolls the
+ // selection bounds into view, not the whole input.
+ assert_true(input.getBoundingClientRect().bottom > 100, "Scroll-padding should be respected");
+}, "Test scrolling into view when typing");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/direction-rtl.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/direction-rtl.html
new file mode 100644
index 0000000000..85724c31fe
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/direction-rtl.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<title>
+ Scrollers should snap to the closest snap point on initial layout (using 'direction: rtl')
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<link rel="match" href="snap-after-initial-layout-ref.html" />
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+ direction: rtl;
+}
+
+#close-target {
+ width: 200px;
+ height: 200px;
+ border: solid green 50px;
+ top: 50px;
+ right: 150px;
+ margin: 50px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+#far-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ right: 700px;
+ background-color: red;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="close-target"></div>
+ <div id="far-target"></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000-ref.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000-ref.html
new file mode 100644
index 0000000000..f3eaa06ac9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000-ref.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>
+ CSS Scroll Snap Reference
+</title>
+<style>
+
+.scroller {
+ width: 100px;
+ height: 100px;
+ border: solid blue;
+ margin: 10px;
+ display: inline-block;
+}
+
+.scroller > div {
+ width: 30px;
+ height: 30px;
+ background: orange;
+}
+
+.proxfar {
+ border-color: orange;
+}
+
+</style>
+
+<p>Test passes if there is an orange square precisely at the top left corner of each blue box (no gap),
+and each orange box is empty.
+
+
+<div class="mandatory">
+ <div class="scroller">
+ <div></div>
+ </div>
+
+ <div class="scroller">
+ <div></div>
+ </div>
+
+ <div class="scroller">
+ <div></div>
+ </div>
+
+ <!-- on-screen -->
+ <div class="scroller">
+ <div></div>
+ </div>
+
+ <div class="scroller">
+ <div></div>
+ </div>
+
+ <div class="scroller">
+ <div></div>
+ </div>
+</div>
+
+<div class="proximity">
+ <!-- off-screen -->
+ <div class="scroller proxfar">
+ </div>
+
+ <div class="scroller proxfar">
+ </div>
+
+ <div class="scroller proxfar">
+ </div>
+
+ <!-- on-screen -->
+ <div class="scroller">
+ <div></div>
+ </div>
+
+ <div class="scroller">
+ <div></div>
+ </div>
+
+ <div class="scroller">
+ <div></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html
new file mode 100644
index 0000000000..ea47c9f36c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<title>
+ On-screen vs. Off-screen Snapped Initial Scroll Position (Mandatory and Proximity)
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap">
+<link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
+<!-- Test assumes 2px proximity is enough to snap.
+ If your implementation has a cogent argument as to why this is too much,
+ please request a change to this test. ~fantasai -->
+<link rel="match" href="scroll-snap-initial-layout-000-ref.html">
+
+<style>
+
+.scroller {
+ scroll-snap-type: both mandatory;
+ overflow: hidden;
+ scroll-padding: 0;
+ width: 100px;
+ height: 100px;
+ border: solid blue;
+ margin: 10px;
+ display: inline-block;
+}
+
+.mandatory > .scroller {
+ scroll-snap-type: both mandatory;
+}
+
+.proximity > .scroller {
+ scroll-snap-type: both proximity;
+}
+
+.scroller > div {
+ /* padding wrapper */
+ width: 30px;
+}
+
+.scroller > div > div {
+ /* target box */
+ height: 30px;
+ background: orange;
+ scroll-snap-align: start;
+}
+
+.proxfar {
+ border-color: orange;
+}
+.proxfar > div > div {
+ background: red;
+}
+
+</style>
+
+<p>Test passes if there is an orange square precisely at the top left corner of each blue box (no gap),
+and each orange box is empty.
+
+
+<div class="mandatory">
+ <!-- off-screen -->
+ <div class="scroller">
+ <div style="padding: 110px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller">
+ <div style="padding-block: 110px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller">
+ <div style="padding-inline: 110px;"><div class="small-target"></div></div>
+ </div>
+
+ <!-- on-screen -->
+ <div class="scroller">
+ <div style="padding: 90px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller">
+ <div style="padding-block: 90px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller">
+ <div style="padding-inline: 90px;"><div class="small-target"></div></div>
+ </div>
+</div>
+
+<div class="proximity">
+ <!-- off-screen -->
+ <div class="scroller proxfar">
+ <div style="padding: 110px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller proxfar">
+ <div style="padding-block: 110px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller proxfar">
+ <div style="padding-inline: 110px;"><div class="small-target"></div></div>
+ </div>
+
+ <!-- on-screen -->
+ <div class="scroller">
+ <div style="padding: 2px 110px 110px 2px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller">
+ <div style="padding: 2px 110px 110px 2px;"><div class="small-target"></div></div>
+ </div>
+
+ <div class="scroller">
+ <div style="padding: 2px 110px 110px 2px;"><div class="small-target"></div></div>
+ </div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000-ref.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000-ref.html
new file mode 100644
index 0000000000..428eb3facb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000-ref.html
@@ -0,0 +1,188 @@
+<!DOCTYPE html>
+<title>
+ CSS Scroll Snap Reference
+</title>
+
+<style>
+
+.wrapper {
+ /* lay out in a nice grid */
+ display: grid;
+ gap: 0.25em;
+ grid-template-columns: repeat(6, max-content);
+}
+
+.scroller {
+ width: 50px;
+ height: 50px;
+ border: solid silver;
+ border-block-start-color: blue;
+ border-inline-start-color: blue;
+ position: relative;
+}
+
+.target {
+ width: 30px;
+ height: 30px;
+ background: orange;
+ top: 0; left: 0; right: 0; bottom: 0;
+ position: absolute;
+}
+
+.TB { writing-mode: horizontal-tb; }
+.LR { writing-mode: vertical-lr; }
+.RL { writing-mode: vertical-rl; }
+.ltr { direction: ltr; }
+.rtl { direction: rtl; }
+
+/* not absolutizing the border colors, so that the test passes even if css-logical is not supported; */
+.large.invert {
+ border: solid silver;
+ border-block-end-color: blue;
+ border-inline-end-color: blue;
+}
+
+.TB.large.invert .target::before { top: auto; }
+.LR.large.invert .target::before { left: auto; }
+.RL.large.invert .target::before { right: auto; }
+
+.TB.ltr.large.invert .target::before { left: auto; }
+.TB.rtl.large.invert .target::before { right: auto; }
+.LR.ltr.large.invert .target::before { top: auto; }
+.LR.rtl.large.invert .target::before { bottom: auto; }
+.RL.ltr.large.invert .target::before { top: auto; }
+.RL.rtl.large.invert .target::before { bottom: auto; }
+
+.large.invert .target::before {
+ width: 9px;
+ height: 9px;
+ background: orange;
+ top: 0; left: 0; right: 0; bottom: 0;
+ position: absolute;
+ content: '';
+}
+
+.large.invert .target {
+ display: block;
+ background: none;
+ width: 30px;
+ height: 30px;
+ border-block-start: 20px solid red;
+ border-inline-start: 20px solid red;
+}
+
+</style>
+
+<p>Test passes if there is an orange square tucked into each blue corner without gaps,
+and there is no red, except for the large inverted cases which should have red
+in the silver corner and smaller orange boxes in the blue corner.
+
+<div class="wrapper">
+<!-- Simple Small Cases -->
+
+<div class="scroller TB ltr small">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR ltr small">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL ltr small">
+ <div class="target"></div>
+</div>
+
+<div class="scroller TB rtl small">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR rtl small">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL rtl small">
+ <div class="target"></div>
+</div>
+
+<!-- Target-inverted Small Cases
+ This row should be identical to the previous. -->
+
+<div class="scroller TB ltr small invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR ltr small invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL ltr small invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller TB rtl small invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR rtl small invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL rtl small invert">
+ <div class="target"></div>
+</div>
+
+<!-- Simple Large Cases -->
+
+<div class="scroller TB ltr large">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR ltr large">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL ltr large">
+ <div class="target"></div>
+</div>
+
+<div class="scroller TB rtl large">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR rtl large">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL rtl large">
+ <div class="target"></div>
+</div>
+
+<!-- Target-inverted Large Cases
+ This is the fun one. -->
+
+<div class="scroller TB ltr large invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR ltr large invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL ltr large invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller TB rtl large invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller LR rtl large invert">
+ <div class="target"></div>
+</div>
+
+<div class="scroller RL rtl large invert">
+ <div class="target"></div>
+</div>
+
+</div> <!-- wrapper -->
+
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000.html
new file mode 100644
index 0000000000..f44317c858
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/scroll-snap-writing-mode-000.html
@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<title>
+ scroll-snap-align vs writing-mode
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap">
+<link rel="match" href="scroll-snap-writing-mode-000-ref.html">
+<link rel="author" title="Elika J. Etemad" href="http://fantasai.inkedblade.net/contact">
+<style>
+
+.wrapper {
+ /* lay out in a nice grid */
+ display: grid;
+ gap: 0.25em;
+ grid-template-columns: repeat(6, max-content);
+}
+
+.scroller {
+ scroll-snap-type: both mandatory;
+ overflow: hidden;
+ scroll-padding: 0;
+ width: 50px;
+ height: 50px;
+ border: solid silver;
+ border-block-start-color: blue;
+ border-inline-start-color: blue;
+}
+.area {
+ width: 200px;
+ height: 200px;
+}
+
+.target {
+ margin: 5px;
+ scroll-snap-align: start;
+}
+
+.small .target {
+ width: 30px;
+ height: 30px;
+ background: orange;
+}
+
+.large .target {
+ width: 51px;
+ height: 51px;
+ border-block-end: 20px solid red;
+ border-inline-end: 20px solid red;
+}
+
+.large .target::before {
+ content: '';
+ display: block;
+ width: 30px;
+ height: 30px;
+ background: orange;
+}
+
+.TB { writing-mode: horizontal-tb; }
+.LR { writing-mode: vertical-lr; }
+.RL { writing-mode: vertical-rl; }
+.ltr { direction: ltr; }
+.rtl { direction: rtl; }
+
+.TB.ltr.invert .target { writing-mode: vertical-rl; direction: rtl; }
+.TB.rtl.invert .target { writing-mode: vertical-lr; direction: rtl; }
+.LR.ltr.invert .target { writing-mode: vertical-rl; direction: rtl; }
+.LR.rtl.invert .target { writing-mode: vertical-rl; direction: ltr; }
+.RL.ltr.invert .target { writing-mode: vertical-lr; direction: rtl; }
+.RL.rtl.invert .target { writing-mode: horizontal-tb; direction: ltr; }
+
+.large.invert {
+ /* key off target‘s writing mode, which we just inverted */
+ border: solid silver;
+ border-block-end-color: blue;
+ border-inline-end-color: blue;
+}
+</style>
+
+<p>Test passes if there is an orange square tucked into each blue corner without gaps,
+ and there is no red, except for the large inverted cases which should have red
+ in the silver corner and smaller orange boxes in the blue corner.
+
+<div class="wrapper">
+<!-- Simple Small Cases -->
+
+<div class="scroller TB ltr small">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR ltr small">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL ltr small">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller TB rtl small">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR rtl small">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL rtl small">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<!-- Target-inverted Small Cases
+ This row should be identical to the previous. -->
+<div class="scroller TB ltr small invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR ltr small invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL ltr small invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller TB rtl small invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR rtl small invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL rtl small invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<!-- Simple Large Cases -->
+
+<div class="scroller TB ltr large">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR ltr large">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL ltr large">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller TB rtl large">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR rtl large">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL rtl large">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<!-- Target-inverted Large Cases
+ This is the fun one. -->
+
+<div class="scroller TB ltr large invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR ltr large invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL ltr large invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller TB rtl large invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller LR rtl large invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+<div class="scroller RL rtl large invert">
+ <div class="area">
+ <div class="target"></div>
+ </div>
+</div>
+
+</div> <!-- wrapper -->
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/snap-after-initial-layout-ref.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/snap-after-initial-layout-ref.html
new file mode 100644
index 0000000000..c8009b626c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/snap-after-initial-layout-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>Reference</title>
+<style>
+div {
+ margin: 0;
+ position: absolute;
+}
+
+#target {
+ width: 300px;
+ height: 300px;
+ top: 0;
+ left: 200px;
+ background-color: green;
+}
+</style>
+
+<div>
+ <div id="target"></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-horizontal-tb.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-horizontal-tb.html
new file mode 100644
index 0000000000..9a680d10d9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-horizontal-tb.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<title>
+ Scrollers should snap to the closest snap point on initial layout
+ (using 'writing-mode: horizontal-tb')
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<link rel="match" href="snap-after-initial-layout-ref.html" />
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+ writing-mode: horizontal-tb;
+}
+
+#close-target {
+ width: 200px;
+ height: 200px;
+ border: solid green 50px;
+ top: 50px;
+ left: 150px;
+ margin: 50px;
+ background-color: green;
+ scroll-snap-align: start end;
+}
+
+#far-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 500px;
+ background-color: red;
+ scroll-snap-align: start end;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="close-target"></div>
+ <div id="far-target"></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-lr.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-lr.html
new file mode 100644
index 0000000000..f4de0411e0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-lr.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<title>
+ Scrollers should snap to the closest snap point on initial layout
+ (using 'writing-mode: vertical-lr')
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<link rel="match" href="snap-after-initial-layout-ref.html" />
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+ writing-mode: vertical-lr;
+}
+
+#close-target {
+ width: 200px;
+ height: 200px;
+ border: solid green 50px;
+ top: 50px;
+ left: 150px;
+ margin: 50px;
+ background-color: green;
+ scroll-snap-align: end start;
+}
+
+#far-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 500px;
+ background-color: red;
+ scroll-snap-align: end start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="close-target"></div>
+ <div id="far-target"></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-rl.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-rl.html
new file mode 100644
index 0000000000..1710bc16dd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-initial-layout/writing-mode-vertical-rl.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>
+ Scrollers should snap to the closest snap point on initial layout
+ (using 'writing-mode: vertical-rl')
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<link rel="match" href="snap-after-initial-layout-ref.html" />
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ /* Chrome bug using 'position:absolute' with LayoutNG disabled */
+ position: relative;
+
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+ writing-mode: vertical-rl;
+}
+
+#close-target {
+ width: 200px;
+ height: 200px;
+ border: solid green 50px;
+ top: 50px;
+ left: 150px;
+ margin: 50px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+#far-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 500px;
+ background-color: red;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="close-target"></div>
+ <div id="far-target"></div>
+</div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.html
new file mode 100644
index 0000000000..53141707bb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-only-snap-area.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>
+ Adding a new snap area when there are none should make the scroller snap to it.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 100px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="target"></div>
+</div>
+
+<script>
+const target = document.getElementById("target");
+const scroller = document.getElementById("scroller");
+
+test(() => {
+ scroller.removeChild(target);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ scroller.appendChild(target);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Adding a new snap area when there are none should make the scroller snap to it.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-snap-area-while-snapped.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-snap-area-while-snapped.html
new file mode 100644
index 0000000000..d26359658f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/adding-snap-area-while-snapped.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>
+ Adding a new snap area while already snapped should not make the scroller snap to it.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#initial-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 100px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+#other-target {
+ width: 300px;
+ height: 300px;
+ top: 300px;
+ left: 300px;
+ background-color: red;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="initial-target"></div>
+ <div id="other-target"></div>
+</div>
+
+<script>
+const initial_target = document.getElementById("initial-target");
+const other_target = document.getElementById("other-target");
+const scroller = document.getElementById("scroller");
+
+test(() => {
+ scroller.removeChild(other_target);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ scroller.appendChild(other_target);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Adding a new snap area while already snapped should not make the scroller snap to it.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.tentative.html
new file mode 100644
index 0000000000..ddea570551
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align-nested.tentative.html
@@ -0,0 +1,119 @@
+<!DOCTYPE html>
+<title>
+ Updating the snap alignment of a snap container's content should make the snap
+ container resnap accordingly.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 200px;
+ width: 200px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#initial-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 100px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+#other-target {
+ width: 300px;
+ height: 300px;
+ top: 300px;
+ left: 300px;
+ background-color: red;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap-area {
+ scroll-snap-align: start !important;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="initial-target"></div>
+ <div id="other-target"></div>
+</div>
+
+<script>
+const initial_target = document.getElementById("initial-target");
+const other_target = document.getElementById("other-target");
+const scroller = document.getElementById("scroller");
+
+function cleanup() {
+ initial_target.style.setProperty("scroll-snap-align", "start");
+ other_target.style.setProperty("scroll-snap-align", "start");
+ initial_target.removeAttribute("class");
+}
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ initial_target.style.setProperty("scroll-snap-align", "end");
+ // scroller maintains scroll position which is still valid as the target's
+ // snap area covers the snap port.
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Changing a large target's snap alignment shouldn't make the scroller" +
+ " resnap if the scroller is already in a valid snap position.");
+
+// Similar to above test case except targets are too small to cover snap port,
+// so scroller must snap in response to change in scroll-snap-align.
+test(t => {
+ t.add_cleanup(cleanup);
+ const initial_target_height = initial_target.offsetHeight;
+ const initial_target_width = initial_target.offsetWidth;
+ const other_target_height = initial_target.offsetHeight;
+ const other_target_width = initial_target.offsetWidth;
+ t.add_cleanup(() => {
+ initial_target.style.setProperty("height", `${initial_target_height}px`);
+ initial_target.style.setProperty("width", `${initial_target_width}px`);
+ other_target.style.setProperty("height", `${other_target_height}px`);
+ other_target.style.setProperty("width", `${other_target_width}px`);
+ })
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ initial_target.style.setProperty("height", `${scroller.clientHeight * 2/3 }px`);
+ initial_target.style.setProperty("width", `${scroller.clientWidth * 2/3 }px`);
+ other_target.style.setProperty("height", `${scroller.clientHeight * 2/3 }px`);
+ other_target.style.setProperty("width", `${scroller.clientWidth * 2/3 }px`);
+
+ // scroll (and snap) to top left of other target.
+ scroller.scrollTo(other_target.offsetTop,
+ other_target.offsetLeft);
+ assert_equals(scroller.scrollTop, other_target.offsetTop,);
+ assert_equals(scroller.scrollLeft, other_target.offsetLeft);
+
+ other_target.style.setProperty("scroll-snap-align", "end");
+ // should be scrolled so as to align scroller's bottom-right with
+ // other_target's bottom-right.
+ assert_equals(scroller.scrollTop,
+ other_target.offsetTop + other_target.offsetHeight - scroller.clientHeight);
+ assert_equals(scroller.scrollLeft,
+ other_target.offsetLeft + other_target.offsetWidth - scroller.clientWidth);
+}, "Changing the current (non-covering) target's snap alignment should make " +
+ "the scroller snap according to the new alignment.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html
new file mode 100644
index 0000000000..a625621c27
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-align.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<title>
+ Updating the snap alignment of a snap container's content should make the snap
+ container resnap accordingly.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 200px;
+ width: 200px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#initial-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 100px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+#other-target {
+ width: 300px;
+ height: 300px;
+ top: 300px;
+ left: 300px;
+ background-color: red;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap-area {
+ scroll-snap-align: start !important;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="initial-target"></div>
+ <div id="other-target"></div>
+</div>
+
+<script>
+const initial_target = document.getElementById("initial-target");
+const other_target = document.getElementById("other-target");
+const scroller = document.getElementById("scroller");
+
+function cleanup() {
+ initial_target.style.setProperty("scroll-snap-align", "start");
+ other_target.style.setProperty("scroll-snap-align", "start");
+ initial_target.removeAttribute("class");
+}
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ initial_target.style.setProperty("scroll-snap-align", "none");
+ assert_equals(scroller.scrollTop, 300);
+ assert_equals(scroller.scrollLeft, 300);
+}, "Removing the current target's snap alignment should make the scroller"
++ " resnap to a new snap area.");
+
+test(t => {
+ t.add_cleanup(cleanup);
+ initial_target.style.setProperty("scroll-snap-align", "none");
+ other_target.style.setProperty("scroll-snap-align", "none");
+
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ initial_target.style.setProperty("scroll-snap-align", "start");
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Changing an element snap alignment from none to start should make the"
++ "scroller resnap.");
+
+test(t => {
+ t.add_cleanup(cleanup);
+ initial_target.style.setProperty("scroll-snap-align", "none");
+ other_target.style.setProperty("scroll-snap-align", "none");
+
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ initial_target.classList.add("snap-area");
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Changing an element snap alignment from none to start by adding a class"
++ " should make the scroller resnap.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.html
new file mode 100644
index 0000000000..c86f39b9d6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type-on-root-element.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+<title>
+ Updating the scroll-snap-type of the root element should make it resnap accordingly.
+ This is another vairant of changing-scroll-snap-type.html for the root element.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+html {
+ overflow: hidden;
+ scroll-snap-type: none;
+}
+
+#y-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 0;
+ background-color: green;
+ scroll-snap-align: start none;
+}
+
+#x-target {
+ width: 300px;
+ height: 300px;
+ top: 0;
+ left: 100px;
+ background-color: red;
+ scroll-snap-align: none start;
+}
+
+.area {
+ width: 1000vw;
+ height: 1000vh;
+}
+</style>
+
+<div class="area"></div>
+<div id="x-target"></div>
+<div id="y-target"></div>
+
+<script>
+const x_target = document.getElementById("x_target");
+const y_target = document.getElementById("y_target");
+const scroller = document.documentElement;
+
+function cleanup() {
+ scroller.style.setProperty("scroll-snap-type", "none");
+}
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "y mandatory");
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 0);
+}, "Changing the scroller's snap type to y should make it resnap on the y-axis.");
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "x mandatory");
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 0);
+}, "Changing the scroller's snap type to x should make it resnap on the x-axis.");
+
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "x mandatory");
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "y mandatory");
+ assert_equals(scroller.scrollTop, 100);
+}, "Changing the scroller's snap type axis should make it resnap.");
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html
new file mode 100644
index 0000000000..70774b3d40
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/changing-scroll-snap-type.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<title>
+ Updating the scroll-snap-type of a snap container should make it resnap accordingly.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: none;
+}
+
+#y-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 0;
+ background-color: green;
+ scroll-snap-align: start none;
+}
+
+#x-target {
+ width: 300px;
+ height: 300px;
+ top: 0;
+ left: 100px;
+ background-color: red;
+ scroll-snap-align: none start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="x-target"></div>
+ <div id="y-target"></div>
+</div>
+
+<script>
+const x_target = document.getElementById("x_target");
+const y_target = document.getElementById("y_target");
+const scroller = document.getElementById("scroller");
+
+function cleanup() {
+ scroller.style.setProperty("scroll-snap-type", "none");
+}
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "y mandatory");
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 0);
+}, "Changing the scroller's snap type to y should make it resnap on the y-axis.");
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "x mandatory");
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 0);
+}, "Changing the scroller's snap type to x should make it resnap on the x-axis.");
+
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "x mandatory");
+ assert_equals(scroller.scrollLeft, 100);
+ assert_equals(scroller.scrollTop, 0);
+
+ scroller.style.setProperty("scroll-snap-type", "y mandatory");
+ assert_equals(scroller.scrollTop, 100);
+}, "Changing the scroller's snap type axis should make it resnap.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/focus-element-no-snap.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/focus-element-no-snap.html
new file mode 100644
index 0000000000..9ec004c628
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/focus-element-no-snap.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<title>Resnap to focused element after relayout</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#snapper {
+ width: 100px;
+ height: 200px;
+ overflow-x: scroll;
+ scroll-snap-type: x mandatory;
+ color: white;
+ background-color: oldlace;
+ display: flex;
+ align-items: center;
+}
+.child {
+ margin-right: 0.5rem;
+ height: 90%;
+ scroll-snap-align: start;
+ padding: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ width: 100px;
+ height: 100px;
+ background-color: indigo;
+}
+</style>
+
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap">
+
+<div id=snapper>
+ <div class="child no-snap" tabindex=-1></div>
+ <div class=child></div>
+ <div class="child" id ="focus" tabindex=-1></div>
+ <div class="child" tabindex=-1></div>
+ <div class=child></div>
+ <div class=child></div>
+</div>
+
+<script>
+
+test(t => {
+ document.getElementById("focus").focus();
+ const element = document.getElementById("snapper");
+ element.style.width = "101px";
+ assert_equals(element.scrollLeft, 0);
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/move-current-target.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/move-current-target.html
new file mode 100644
index 0000000000..ccadc884c5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/move-current-target.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<title>
+ Moving the current snap target should make the scroller resnap to it.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: relative;
+ margin: 0;
+}
+
+#block {
+ height: 100px;
+ width: 100px;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#initial-target {
+ width: 300px;
+ height: 300px;
+ left: 100px;
+ top: 0;
+ transform: none;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+#other-target {
+ width: 300px;
+ height: 300px;
+ left: 300px;
+ background-color: red;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div id="block"></div>
+ <div id="initial-target"></div>
+ <div id="other-target"></div>
+ <div class="area"></div>
+</div>
+
+<script>
+const initial_target = document.getElementById("initial-target");
+const other_target = document.getElementById("other-target");
+const block = document.getElementById("block");
+const scroller = document.getElementById("scroller");
+
+function cleanup() {
+ initial_target.style.setProperty("transform", "none");
+ initial_target.style.setProperty("top", "0");
+ block.style.setProperty("height", "100px");
+}
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ initial_target.style.setProperty("top", "300px");
+ assert_equals(scroller.scrollTop, 400);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Moving the current snap target should make the scroller resnap to it.");
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ block.style.setProperty("height", "200px");
+ assert_equals(scroller.scrollTop, 200);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Changing the layout of other elements should be able to cause resnapping to \
+the target.");
+
+test(t => {
+ t.add_cleanup(cleanup);
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ initial_target.style.setProperty("transform", "translate(0,100px)");
+ assert_equals(scroller.scrollTop, 200);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Transforming the current snap target should make the scroller resnap to it.");
+
+test(t => {
+ t.add_cleanup(cleanup);
+ initial_target.style.setProperty("top", "100px");
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 200);
+ assert_equals(scroller.scrollLeft, 100);
+
+ initial_target.style.setProperty("transform", "translate(0,100px)");
+ initial_target.style.setProperty("top", "0");
+ assert_equals(scroller.scrollTop, 200);
+ assert_equals(scroller.scrollLeft, 100);
+}, "Applying two property changes that do not change the visual offset of the \
+target should not change the scroll offset.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element.html
new file mode 100644
index 0000000000..f15a291f08
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-element.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="resources/common.js" ></script>
+ </head>
+ <body>
+ <style>
+ .scroller {
+ overflow: scroll;
+ position: relative;
+ height: 400px;
+ width: 400px;
+ border:solid 1px black;
+ scroll-snap-type: y mandatory;
+ }
+ .no-snap { scroll-snap-align: none }
+ .scroller div:focus {
+ border: solid 1px red;
+ }
+ .large-space {
+ height: 300vh;
+ width: 300vw;
+ }
+ .target {
+ scroll-snap-align: start;
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ border: solid 1px black;
+ }
+ .top {
+ top: 0px;
+ }
+ .left {
+ left: 0px;
+ }
+ .right {
+ left: 200px;
+ }
+ .bottom {
+ top: 200px;
+ }
+ </style>
+ <div id="scroller" class="scroller">
+ <div class="large-space no-snap" tabindex="1" id="space"></div>
+ <div id="topleft" tabindex="1" class="top left target">top left</div>
+ <div id="topright" tabindex="1" class="top right target">top right</div>
+ <div id="bottomleft" tabindex="1" class="bottom left target">bottom left</div>
+ <div id="bottomright" tabindex="1" class="bottom right target">bottom right</div>
+ </div>
+ <script>
+ window.onload = () => {
+ const bottomright = document.getElementById("bottomright");
+ const bottomleft = document.getElementById("bottomleft");
+ const scroller = document.getElementById("scroller");
+
+ async function commonInitialization() {
+ await waitForCompositorCommit();
+ assert_equals(scroller.scrollTop, 0, "snapped to top row");
+ }
+
+ promise_test(async (t) => {
+ await commonInitialization();
+
+ focusAndAssert(bottomright);
+ await runScrollSnapSelectionVerificationTest(t, scroller,
+ [bottomright,
+ bottomleft],
+ /*expected_target=*/bottomright, "y");
+
+ focusAndAssert(bottomleft);
+ await runScrollSnapSelectionVerificationTest(t, scroller,
+ [bottomright,
+ bottomleft],
+ /*expected_target=*/bottomleft, "y");
+ }, "scroller selects focused target from aligned choices on snap");
+
+ promise_test(async (t) => {
+ t.add_cleanup(() => {
+ bottomright.style.left = "200px";
+ })
+ await commonInitialization();
+
+ // Move bottomright out of the snapport.
+ bottomright.style.left = "500px";
+
+ // Set focus on bottomright without scrolling to it.
+ focusAndAssert(bottomright, true);
+ await runScrollSnapSelectionVerificationTest(t, scroller,
+ [bottomright,
+ bottomleft],
+ /*expected_target=*/bottomleft, "y");
+ }, "out-of-viewport focused element is not the selected snap target.");
+
+ promise_test(async(t) => {
+ t.add_cleanup(() => {
+ bottomleft.style.top = "200px";
+ });
+ await commonInitialization();
+
+ // Set focus on bottomright without scrolling to it.
+ focusAndAssert(bottomright, true);
+
+ // Move bottomleft below bottomright.
+ bottomleft.style.top = "400px";
+
+ // Snap to bottomleft.
+ scroller.scrollTop = bottomleft.offsetTop;
+
+ // Test that if bottomright is also shifted so that it is aligned with
+ // bottomleft, bottomleft remains the selected snap target, despite
+ // bottomright's having focus.
+ await runLayoutSnapSeletionVerificationTest(t, scroller, [bottomright],
+ bottomleft, "y");
+ }, "scroller follows selected snap target through layout shift," +
+ "regardless of focus");
+
+ }
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-nested-containers.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-nested-containers.html
new file mode 100644
index 0000000000..a6a087316f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/prefer-focused-nested-containers.html
@@ -0,0 +1,109 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="resources/common.js" ></script>
+ </head>
+ <body>
+ <style>
+ .snap {
+ scroll-snap-align: start;
+ }
+ .placeholder {
+ height: 40%;
+ width: 40%;
+ position: absolute;
+ top: 0px;
+ border: solid 1px black;
+ }
+ .right {
+ left: 50%;
+ }
+ .container {
+ position: relative;
+ border: solid 1px blue;
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .bigcontainer {
+ height: 1000px;
+ width: 1000px;
+ }
+ .smallcontainer {
+ height: 400px;
+ width: 400px;
+ position: absolute;
+ border: solid 1px blue;
+ top: 500px;
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+ }
+ .large-space {
+ height: 300vh;
+ width: 300vw;
+ position: absolute;
+ }
+ .target {
+ top: 50%;
+ width: 40%;
+ height: 40%;
+ position: absolute;
+ border: solid 1px red;
+ }
+ .target:focus {
+ border:solid 2px green;
+ }
+ </style>
+ <div class="bigcontainer container" id="outercontainer">
+ <div class="large-space"></div>
+ <div id="leftplaceholder" class="snap placeholder">LPH (outer)</div>
+ <div id="rightplaceholder" class="snap placeholder right">RPH (outer)</div>
+ <div id="leftcontainer" class="snap smallcontainer">
+ <div class="large-space"></div>
+ <div class="snap placeholder"></div>
+ <div class="snap placeholder right"></div>
+ <div id="lefttarget1" tabindex="1" class="snap target"></div>
+ <div id="lefttarget2" tabindex="1" class="snap target right"></div>
+ </div>
+ <div id="rightcontainer" class="snap smallcontainer right">
+ <div class="large-space"></div>
+ <div class="snap placeholder"></div>
+ <div class="snap placeholder right"></div>
+ <div id="righttarget1" tabindex="1" class="snap target"></div>
+ <div id="righttarget2" tabindex="1" class="snap target right"></div>
+ </div>
+ </div>
+ <script>
+ // This test verifies that a snap container (outer) which contains another
+ // snap container (inner) snaps with awareness of focus on children of the
+ // inner container, i.e. outer should prefer to select the snap area whose
+ // child has focus even if there is an intermediate snap container between
+ // the child and outer.
+ window.onload = () => {
+ const lefttarget1 = document.getElementById("lefttarget1");
+ const righttarget1 = document.getElementById("righttarget1");
+ const leftcontainer = document.getElementById("leftcontainer");
+ const rightcontainer = document.getElementById("rightcontainer");
+ const outercontainer = document.getElementById("outercontainer");
+
+ promise_test(async (t) => {
+ await waitForCompositorCommit();
+
+ focusAndAssert(lefttarget1, /*preventScroll=*/true);
+ await runScrollSnapSelectionVerificationTest(t, outercontainer,
+ [leftcontainer, rightcontainer], leftcontainer, "y");
+
+ focusAndAssert(righttarget1, /*preventScroll=*/true);
+ await runScrollSnapSelectionVerificationTest(t, outercontainer,
+ [leftcontainer, rightcontainer], rightcontainer, "y");
+ }, "Snap container prefers focused nested snap target.");
+ }
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/resources/common.js b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/resources/common.js
new file mode 100644
index 0000000000..6ceec9118c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/multiple-aligned-targets/resources/common.js
@@ -0,0 +1,148 @@
+// Utility functions for scroll snap tests which verify User-Agents' snap point
+// selection logic when multiple snap targets are aligned.
+// It depends on methods in /resources/testdriver-actions.js and
+// /dom/event/scrolling/scroll_support.js so html files using these functions
+// should include those files as <script>s.
+
+// This function should be used by scroll snap WPTs wanting to test snap target
+// selection when scrolling to multiple aligned targets.
+// It assumes scroll-snap-align: start alignment and tries to align to the list
+// of snap targets provided, |elements|, which are all expected to be at the
+// same offset.
+async function scrollToAlignedElementsInAxis(scroller, elements, axis) {
+ let target_offset_y = null;
+ let target_offset_x = null;
+ if (axis == "y") {
+ for (const e of elements) {
+ if (target_offset_y) {
+ assert_equals(e.offsetTop, target_offset_y,
+ `${e.id} is at y offset ${target_offset_y}`);
+ } else {
+ target_offset_y = e.offsetTop;
+ }
+ }
+ assert_equals();
+ } else {
+ for (const e of elements) {
+ if (target_offset_x) {
+ assert_equals(e.offsetLeft, target_offset_x,
+ `${e.id} is at x offset ${target_offset_x}`);
+ } else {
+ target_offset_x = e.offsetLeft;
+ }
+ }
+ }
+ assert_not_equals(target_offset_x || target_offset_y, null);
+
+ const scrollend_promise = waitForScrollendEventNoTimeout(scroller);
+ await new test_driver.Actions().scroll(0, 0,
+ (target_offset_x || 0) - scroller.scrollLeft,
+ (target_offset_y || 0) - scroller.scrollTop,
+ { origin: scroller })
+ .send();
+ await scrollend_promise;
+ if (axis == "y") {
+ assert_equals(scroller.scrollTop, target_offset_y, "vertical scroll done");
+ } else {
+ assert_equals(scroller.scrollLeft,target_offset_x, "horizontal scroll done");
+ }
+}
+
+// This function verifies the snap target that a scroller picked by triggerring
+// a layout change and observing which target is followed. Tests using this
+// method should ensure that there is at least 100px of room to scroll in the
+// desired axis.
+// It assumes scroll-snap-align: start alignment.
+function verifySelectedSnapTarget(scroller, expected_snap_target, axis) {
+ // Save initial style.
+ const initial_left = getComputedStyle(expected_snap_target).left;
+ const initial_top = getComputedStyle(expected_snap_target).top;
+ if (axis == "y") {
+ // Move the expected snap target along the y axis.
+ const initial_scroll_top = scroller.scrollTop;
+ const target_top = expected_snap_target.offsetTop + 100;
+ expected_snap_target.style.top = `${target_top}px`;
+ assert_equals(scroller.scrollTop, expected_snap_target.offsetTop,
+ `scroller followed ${expected_snap_target.id} after layout change`);
+ assert_not_equals(scroller.scrollTop, initial_scroll_top,
+ "scroller actually scrolled in y axis");
+ } else {
+ // Move the expected snap target along the y axis.
+ const initial_scroll_left = scroller.scrollLeft;
+ const target_left = expected_snap_target.offsetLeft + 100;
+ expected_snap_target.style.left = `${target_left}px`;
+ assert_equals(scroller.scrollLeft, expected_snap_target.offsetLeft,
+ `scroller followed ${expected_snap_target.id} after layout change`);
+ assert_not_equals(scroller.scrollLeft, initial_scroll_left,
+ "scroller actually scrolled in x axis");
+ }
+ // Undo style changes.
+ expected_snap_target.style.top = initial_top;
+ expected_snap_target.style.left = initial_left;
+}
+
+// This is a utility function for tests which verify that the correct element
+// is snapped to when snapping at the end of a scroll.
+async function runScrollSnapSelectionVerificationTest(t, scroller, aligned_elements,
+ expected_target, axis) {
+ // Save initial scroll offset.
+ const initial_scroll_left = scroller.scrollLeft;
+ const initial_scroll_top = scroller.scrollTop;
+ await scrollToAlignedElementsInAxis(scroller, aligned_elements, axis);
+ verifySelectedSnapTarget(scroller, expected_target, axis);
+ // Restore initial scroll offsets.
+ const scrollend_promise = new Promise((resolve) => {
+ scroller.addEventListener("scrollend", resolve);
+ });
+ scroller.scrollTo(initial_scroll_left, initial_scroll_top);
+ await scrollend_promise;
+}
+
+// This is a utility function for tests verifying that a layout shift does not
+// cause a scroller to change its selected snap target.
+// It assumes the element to be aligned have scroll-snap-align: start.
+// It tries to align the list of snap targets provided, |elements| with the
+// current snap target.
+function shiftLayoutToAlignElements(elements, target, axis) {
+ for (let element of elements) {
+ if (axis == "y") {
+ element.style.top = `${target.offsetTop}px`;
+ } else {
+ element.style.left = `${target.offsetLeft}px`;
+ }
+ }
+}
+
+// This is a utility function for tests verifying that a layout shift does not
+// cause a scroller to change its selected snap target.
+// It assumes scroll-snap-align: start alignment.
+async function runLayoutSnapSeletionVerificationTest(t, scroller, elements_to_align,
+ expected_target, axis) {
+ // Save initial scroll offsets and position.
+ const initial_scroll_left = scroller.scrollLeft;
+ const initial_scroll_top = scroller.scrollTop;
+ let initial_tops = [];
+ for (const element of elements_to_align) {
+ initial_tops.push(getComputedStyle(element).top);
+ }
+
+ shiftLayoutToAlignElements(elements_to_align, expected_target, axis);
+ verifySelectedSnapTarget(scroller, expected_target, axis);
+
+ // Restore initial scroll offset and position states.
+ let num_elements = initial_tops.length;
+ for (let i = 0; i < num_elements; i++) {
+ elements_to_align[i].style.top = initial_tops[i];
+ }
+ // Restore initial scroll offsets.
+ const scrollend_promise = new Promise((resolve) => {
+ scroller.addEventListener("scrollend", resolve);
+ });
+ scroller.scrollTo(initial_scroll_left, initial_scroll_top);
+ await scrollend_promise;
+}
+
+function focusAndAssert(element, preventScroll=false) {
+ element.focus({preventScroll: preventScroll});
+ assert_equals(document.activeElement, element);
+}
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/not-resnap-outside-proximity-threshold.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/not-resnap-outside-proximity-threshold.html
new file mode 100644
index 0000000000..b2c5720efb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/not-resnap-outside-proximity-threshold.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>
+ Not re-snap once after a scroll operation has finished without snapping
+ because the scroll destination was outside of the snap proximity threshold.
+</title>
+<!-- This test assumes that all major browsers' default scroll-snap proximity
+ thresholds are greater than 200px. -->
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1780154">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: hidden;
+ scroll-snap-type: y proximity;
+}
+
+.snap {
+ width: 300px;
+ height: 300px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div class="snap" style="top: 0px;"></div>
+ <div class="snap" style="top: 500px;"></div>
+</div>
+
+<script>
+test(() => {
+ // The initial snap position is at (0, 0).
+ assert_equals(scroller.scrollTop, 0);
+ assert_equals(scroller.scrollLeft, 0);
+
+ // Scroll to a position where it's outside of the scroll-snap proximity
+ // threshold, so that it won't trigger snapping.
+ scroller.scrollTo(0, 250);
+
+ assert_equals(scroller.scrollTop, 250);
+ assert_equals(scroller.scrollLeft, 0);
+
+ // Changing the initial snap target position, but still it's outside of the
+ // threshold.
+ document.querySelectorAll(".snap")[0].style.top = "10px";
+
+ // Not re-snap to the last snap point.
+ assert_equals(scroller.scrollTop, 250);
+ assert_equals(scroller.scrollLeft, 0);
+}, "Not re-snap once after a scroll operation has finished without snapping " +
+ "because the scroll destination was outside of the snap proximity threshold.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/remove-current-target.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/remove-current-target.html
new file mode 100644
index 0000000000..82bddf8074
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/remove-current-target.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<title>
+ Removing the current snap target should make the scroller snap to a new target.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#initial-target {
+ width: 300px;
+ height: 300px;
+ top: 100px;
+ left: 100px;
+ background-color: red;
+ scroll-snap-align: start;
+}
+
+#other-target {
+ width: 300px;
+ height: 300px;
+ top: 300px;
+ left: 300px;
+ background-color: green;
+ scroll-snap-align: start;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="initial-target"></div>
+ <div id="other-target"></div>
+</div>
+
+<script>
+const initial_target = document.getElementById("initial-target");
+const other_target = document.getElementById("other-target");
+const scroller = document.getElementById("scroller");
+
+test(() => {
+ scroller.scrollTo(0,0);
+ assert_equals(scroller.scrollTop, 100);
+ assert_equals(scroller.scrollLeft, 100);
+
+ scroller.removeChild(initial_target);
+ assert_equals(scroller.scrollTop, 300);
+ assert_equals(scroller.scrollLeft, 300);
+}, "Removing the current snap target should make the scroller snap to a new target.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.html
new file mode 100644
index 0000000000..637c578a85
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/resnap-to-focused.html
@@ -0,0 +1,82 @@
+<!doctype html>
+<title>Resnap to focused element after relayout</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#snapper {
+ counter-reset: child 0;
+ width: 200px;
+ scroll-snap-type: block mandatory;
+ overflow:hidden;
+ height: 100px;
+}
+.child {
+ width: 100px;
+ height: 100px;
+ background:red;
+ text-align: center;
+ box-sizing: border-box;
+ counter-increment: child;
+ float: left;
+}
+.child.f {
+ background: green;
+ scroll-snap-align: center;
+}
+.child::before {
+ content: counter(child);
+}
+
+</style>
+
+<link rel=author title="Tab Atkins-Bittner" href="https://www.xanthir.com/contact/">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap">
+<!--
+When re-snapping after a layout change,
+if multiple elements were capable of being the snap target previously,
+and one of them is focused,
+you must resnap to the focused one.
+-->
+<div id=snapper>
+ <div class="child no-snap" tabindex=-1></div>
+ <div class=child></div>
+ <div class="child f" tabindex=-1></div>
+ <div class="child f" tabindex=-1></div>
+ <div class=child></div>
+ <div class=child></div>
+</div>
+
+<script>
+
+var container = document.querySelector("#snapper");
+var [one,two] = document.querySelectorAll(".child.f");
+var unsnappable = document.querySelector(".child.no-snap");
+
+async_test(t=>{
+ requestAnimationFrame(()=>{
+ testSnap(t, one, 3);
+ requestAnimationFrame(()=>{
+ testSnap(t, two, 4);
+ requestAnimationFrame(()=>{
+ testSnap(t, one, 3);
+ t.done();
+ });
+ });
+ });
+});
+
+function testSnap(t, child, expectedRow) {
+ t.step(()=>{
+ unsnappable.focus();
+ container.style.width = "200px";
+ var startingRow = container.scrollTop/100 + 1;
+ assert_equals(startingRow, 2, "Initially snapped to row 2");
+ child.focus();
+ container.style.width = "100px";
+ var endingRow = container.scrollTop/100 + 1;
+ assert_equals(endingRow, expectedRow, `After resize, should snap to row ${expectedRow}.`);
+ });
+}
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.html b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.html
new file mode 100644
index 0000000000..fb9469ba73
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-after-relayout/snap-to-different-targets.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<title>
+ The scroller should try to resnap to targets for both axes if possible.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#re-snap" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#x-axis-target {
+ scroll-snap-align: none start;
+ background-color: blue;
+ width: 100px;
+ height: 100px;
+ top: 400px;
+ left: 200px;
+}
+
+#y-axis-target {
+ scroll-snap-align: start none;
+ background-color: green;
+ width: 100px;
+ height: 100px;
+ top: 200px;
+ left: 400px;
+}
+
+#far-x-axis-target {
+ scroll-snap-align: none start;
+ background-color: blue;
+ width: 100px;
+ height: 100px;
+ top: 1200px;
+ left: 300px;
+}
+
+#far-y-axis-target {
+ scroll-snap-align: start none;
+ background-color: green;
+ width: 100px;
+ height: 100px;
+ top: 300px;
+ left: 1200px;
+}
+
+.area {
+ width: 2000px;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div id="x-axis-target"></div>
+ <div id="y-axis-target"></div>
+ <div id="far-x-axis-target"></div>
+ <div id="far-y-axis-target"></div>
+</div>
+
+<script>
+
+const x_target = document.getElementById("x-axis-target");
+const y_target = document.getElementById("y-axis-target");
+const scroller = document.getElementById("scroller");
+
+test(t => {
+ // The scroller should be snapped to the two closest points on first layout.
+ assert_equals(scroller.scrollTop, 200);
+ assert_equals(scroller.scrollLeft, 200);
+ x_target.style.setProperty("left", "1000px");
+ y_target.style.setProperty("top", "1000px");
+
+ // The style change makes it impossible for the scroller to snap to both
+ // targets, but at least one of the targets should be preserved. The scroller
+ // should then re-evaluate the snap point for the other axis.
+ const snapped_to_x = scroller.scrollLeft == 1000 && scroller.scrollTop == 300;
+ const snapped_to_y = scroller.scrollTop == 1000 && scroller.scrollLeft == 300;
+ assert_true(snapped_to_x || snapped_to_y);
+}, "Scroller should snap to at least one of the targets if unable to snap to both after a layout change.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-add-scroll-container.html b/testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-add-scroll-container.html
new file mode 100644
index 0000000000..66fa96b745
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-add-scroll-container.html
@@ -0,0 +1,154 @@
+<!DOCTYPE html>
+<title>
+ Adding a scrollable element should make it start capturing snap points.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#captures-snap-positions"/>
+<meta name="viewport" content="user-scalable=no">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+html {
+ scroll-snap-type: y mandatory;
+}
+
+body {
+ margin: 0px;
+}
+
+#middle-scroller {
+ top: 100px;
+ height: 500px;
+ width: 500px;
+ overflow: visible;
+ background-color: rgb(12, 61, 2);
+ scroll-snap-type: none;
+}
+
+#inner-scroller {
+ top: 200px;
+ height: 400px;
+ width: 400px;
+ overflow: visible;
+ background-color: rgb(65, 139, 50);
+ scroll-snap-type: y mandatory;
+}
+
+.space {
+ width: 2000px;
+ height: 2000px;
+}
+
+#inner-snap-area {
+ top: 300px;
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#document-snap-area {
+ top: 500px;
+ width: 200px;
+ height: 200px;
+ background-color: lightblue;
+ scroll-snap-align: start;
+}
+
+#inserted-snap-container {
+ top: 400px;
+ height: 600px;
+ width: 400px;
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+}
+</style>
+
+<div class="space"></div>
+ <div id="middle-scroller">
+ <div class="space"></div>
+ <div id="inner-scroller">
+ <div class="space"></div>
+ <div id="inner-snap-area"></div>
+ </div>
+ </div>
+</div>
+<div id="document-snap-area"></div>
+<script>
+
+const inner_scroller = document.getElementById("inner-scroller");
+const middle_scroller = document.getElementById("middle-scroller");
+const document_scroller = document.scrollingElement;
+
+// This tests that making an element scrollable will reassign the correct snap
+// areas to itself, per spec [1].
+// [1] https://drafts.csswg.org/css-scroll-snap/#captures-snap-positions
+test(() => {
+ // Confirm that the document-level scroller is the snap container for all of
+ // the snap areas.
+ document_scroller.scrollTo(0, 10);
+ assert_equals(document_scroller.scrollTop, 500);
+ // Snaps to the inner snap area.
+ document_scroller.scrollBy(0, 75);
+ assert_equals(document_scroller.scrollTop, 600);
+
+ // The middle scroller should now have the inner snap area assigned to it.
+ // Per spec, even if the snap-type is 'none', it should still capture snap
+ // points.
+ middle_scroller.style.setProperty("overflow", "scroll");
+
+ // The middle scroller has snap-type 'none' so it should not snap.
+ middle_scroller.scrollBy(0, 10);
+ assert_equals(middle_scroller.scrollTop, 10);
+
+ // The document scroller should only snap to the document-level snap area.
+ document_scroller.scrollTo(0, 600);
+ assert_equals(document_scroller.scrollTop, 500);
+
+ // The inner scroller should now have the innermost snap area assigned to it.
+ inner_scroller.style.setProperty("overflow", "scroll");
+ inner_scroller.scrollBy(0, 10);
+ assert_equals(inner_scroller.scrollTop, 300);
+
+ document_scroller.scrollTo(0, 600);
+ assert_equals(document_scroller.scrollTop, 500);
+
+}, "Making an element scrollable should make it capture the correct descendant\
+ snap areas' snap points.");
+
+ // Test that attaching a new snap container also properly assigns snap areas.
+ test(() => {
+ // All containers should capture snap areas.
+ middle_scroller.style.setProperty("overflow", "scroll");
+ inner_scroller.style.setProperty("overflow", "scroll");
+
+ // Sanity check that the scrollers still snap to the snap areas.
+ document_scroller.scrollTo(0, 10);
+ inner_scroller.scrollTo(0,10);
+ assert_equals(inner_scroller.scrollTop, 300);
+ assert_equals(document_scroller.scrollTop, 500);
+
+ // Create new snap container and append thedocument-level snap area as its
+ // child.
+ const inserted_scroller = document.createElement("div");
+ inserted_scroller.id = "inserted-snap-container";
+ const space = document.createElement("div");
+ space.classList.add("space");
+ inserted_scroller.appendChild(space);
+ inserted_scroller.appendChild(document.getElementById("document-snap-area"));
+ document_scroller.appendChild(inserted_scroller);
+
+ // Document scroller no longer snaps.
+ document_scroller.scrollTo(0, 400);
+ assert_equals(document_scroller.scrollTop, 400);
+
+ // Inserted scroller snaps.
+ inserted_scroller.scrollTo(0, 10);
+ assert_equals(inserted_scroller.scrollTop, 500);
+ }, "Attaching a new element that is scrollable should assign the correct snap\
+ areas to it.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.html b/testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.html
new file mode 100644
index 0000000000..e3798cc73f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-area-capturing-remove-scroll-container.html
@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<title>
+ When an element no longer captures snap positions (e.g., no longer
+ scrollable), then its currently captured snap areas must be reassigned.
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#captures-snap-positions"/>
+<meta name="viewport" content="user-scalable=no">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+html {
+ scroll-snap-type: y mandatory;
+}
+
+body {
+ margin: 0px;
+}
+
+#middle-scroller {
+ top: 100px;
+ height: 500px;
+ width: 500px;
+ overflow: scroll;
+ background-color: rgb(12, 61, 2);
+ scroll-snap-type: none;
+}
+
+#inner-scroller {
+ top: 200px;
+ height: 400px;
+ width: 400px;
+ overflow: scroll;
+ background-color: rgb(65, 139, 50);
+ scroll-snap-type: y mandatory;
+}
+
+.space {
+ width: 2000px;
+ height: 2000px;
+}
+
+#inner-snap-area {
+ top: 300px;
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#document-snap-area {
+ top: 500px;
+ width: 200px;
+ height: 200px;
+ background-color: lightblue;
+ scroll-snap-align: start;
+}
+
+</style>
+<div class="space"></div>
+ <div id="middle-scroller">
+ <div class="space"></div>
+ <div id="inner-scroller">
+ <div class="space"></div>
+ <div id="inner-snap-area"></div>
+ </div>
+ </div>
+</div>
+<div id="document-snap-area"></div>
+<script>
+
+// This tests that making a snap container no longer scrollable will reassign
+// its snap areas to the next scrollable ancestor, per spec [1].
+// [1] https://drafts.csswg.org/css-scroll-snap/#captures-snap-positions
+test(() => {
+ const inner_scroller = document.getElementById("inner-scroller");
+ const middle_scroller = document.getElementById("middle-scroller");
+ const document_scroller = document.scrollingElement;
+
+ // Inner scroller should snap to its captured area.
+ // Middle scroller doesn't snap.
+ // Document scroller should snap to its only captured area.
+ inner_scroller.scrollBy(0,10);
+ middle_scroller.scrollBy(0, 10);
+ // Scroll to (0,600), where we would expect the inner snap area to be relative
+ // to the document scroller.
+ document_scroller.scrollTo(0, 600);
+ assert_equals(inner_scroller.scrollTop, 300);
+ assert_equals(middle_scroller.scrollTop, 10);
+ assert_equals(document_scroller.scrollTop, 500);
+
+ // Inner scroller is no longer a scroll container.
+ inner_scroller.style.setProperty("overflow", "visible");
+ assert_equals(inner_scroller.scrollTop, 0);
+ assert_equals(middle_scroller.scrollTop, 10);
+ assert_equals(document_scroller.scrollTop, 500);
+
+ // The new snap container is the middle scroller, which has snap-type 'none'.
+ // Per spec, the scroll container should capture snap positions even if it has
+ // snap-type 'none'.
+ // The middle scroller should not snap.
+ // The document scroller should still only snap to its captured snap area.
+ document_scroller.scrollBy(0, 100);
+ middle_scroller.scrollBy(0, 10);
+ assert_equals(inner_scroller.scrollTop, 0);
+ assert_equals(middle_scroller.scrollTop, 20);
+ assert_equals(document_scroller.scrollTop, 500);
+
+ // The scroll container should now be at the document level.
+ middle_scroller.style.setProperty("overflow", "visible");
+ document_scroller.scrollBy(0, -10);
+ assert_equals(inner_scroller.scrollTop, 0);
+ assert_equals(middle_scroller.scrollTop, 0);
+
+ // Check that the existing snap area did not get removed when reassigning
+ // the inner snap area.
+ assert_equals(document_scroller.scrollTop, 500);
+
+ // Check that the inner snap area got reassigned to the document.
+ document_scroller.scrollBy(0, 150);
+ assert_equals(document_scroller.scrollTop, 600);
+}, 'Making a snap container not scrollable should promote the next scrollable\
+ ancestor to become a snap container.');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-at-user-scroll-end.html b/testing/web-platform/tests/css/css-scroll-snap/snap-at-user-scroll-end.html
new file mode 100644
index 0000000000..8643b3c148
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-at-user-scroll-end.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<title>Tests that window should snap at user scroll end.</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="support/common.js"></script>
+<style>
+html {
+ margin: 0px;
+ scroll-snap-type: both mandatory;
+}
+#content {
+ width: 2000px;
+ height: 2000px;
+ padding: 0px;
+ margin: 0px;
+}
+#target {
+ position: relative;
+ left: 400px;
+ top: 400px;
+ width: 400px;
+ height: 400px;
+ background-color: lightblue;
+ overflow: hidden;
+ scroll-snap-align: start;
+}
+#i1 {
+ color: red;
+ font-weight: bold;
+}
+</style>
+
+<div id="content">
+ <div id="target">
+ <h1>CSSScrollSnap</h1>
+ <h4>Tests that the window can snap at user scroll end.</h4>
+ <ol>
+ <li id="i1" style="color: red">
+ Scroll the page vertically and horizontally.
+ Keep scrolling until background has turned yellow.</li>
+ <li id="i2"> Press the button "Done"</li>
+ </ol>
+ <input type="button" id="btn" value="DONE" style="width: 100px; height: 50px;"/>
+ </div>
+</div>
+
+<script>
+var snap_test = async_test('Tests that window should snap at user scroll end.');
+var body = document.body;
+var button = document.getElementById("btn");
+var target = document.getElementById("target");
+var instruction1 = document.getElementById("i1");
+var instruction2 = document.getElementById("i2");
+var scrolled_x = false;
+var scrolled_y = false;
+var start_x = window.scrollX;
+var start_y = window.scrollY;
+var actions_promise;
+
+scrollTop = () => window.scrollY;
+
+window.onscroll = function() {
+ if (scrolled_x && scrolled_y) {
+ body.style.backgroundColor = "yellow";
+ instruction1.style.color = "black";
+ instruction1.style.fontWeight = "normal";
+ instruction2.style.color = "red";
+ instruction2.style.fontWeight = "bold";
+ return;
+ }
+
+ scrolled_x |= window.scrollX != start_x;
+ scrolled_y |= window.scrollY != start_y;
+}
+
+button.onclick = function() {
+ if (!scrolled_x || !scrolled_y)
+ return;
+
+ snap_test.step(() => {
+ assert_equals(window.scrollX, target.offsetLeft,
+ "window.scrollX should be at snapped position.");
+ assert_equals(window.scrollY, target.offsetTop,
+ "window.scrollY should be at snapped position.");
+ });
+
+ // To make the test result visible.
+ var content = document.getElementById("content");
+ body.removeChild(content);
+ actions_promise.then( () => {
+ snap_test.done();
+ });
+}
+
+// Inject scroll actions.
+const pos_x = 20;
+const pos_y = 20;
+const scroll_delta_x = 100;
+const scroll_delta_y = 100;
+actions_promise = new test_driver.Actions()
+ .scroll(pos_x, pos_y, scroll_delta_x, scroll_delta_y)
+ .send().then(() => {
+ return waitForAnimationEnd(scrollTop);
+}).then(() => {
+ return test_driver.click(button);
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-inline-block.html b/testing/web-platform/tests/css/css-scroll-snap/snap-inline-block.html
new file mode 100644
index 0000000000..9606023e16
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-inline-block.html
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+#scroller {
+ width: 400px;
+ height: 350px;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+}
+#space {
+ width: 1000px;
+ height: 1000px;
+}
+#target {
+ width: 200px;
+ height: 200px;
+ left: 300px;
+ top: 300px;
+}
+</style>
+
+<div id="scroller">
+ <div id="space"></div>
+ <div id="target"></div>
+</div>
+
+<script>
+const scroller_width = scroller.clientWidth;
+const scroller_height = scroller.clientHeight;
+[
+ ["horizontal-tb", 300, 500 - scroller_height],
+ ["vertical-lr", 500 - scroller_width, 300],
+ ["vertical-rl", scroller_width - 700, 300]
+].forEach(([writing_mode, left, top]) => {
+ test(() => {
+ const target_left = getComputedStyle(target).left;
+ scroller.style.writingMode = writing_mode;
+ target.style.scrollSnapAlign = "end start";
+ if (writing_mode == "vertical-rl") {
+ target.style.left = (scroller_width - 700) + "px";
+ scroller.scrollTo(-500, 0);
+ } else {
+ scroller.scrollTo(0, 0);
+ }
+ assert_equals(scroller.scrollLeft, left, "aligns correctly on x");
+ assert_equals(scroller.scrollTop, top, "aligns correctly on y");
+ target.style.left = target_left;
+ scroller.style.writingMode = "";
+ }, "Snaps correctly for " + writing_mode +
+ " writing mode with 'scroll-snap-align: end start' alignment");
+});
+
+[
+ ["horizontal-tb", 500 - scroller_width, 300],
+ ["vertical-lr", 300, 500 - scroller_height],
+ ["vertical-rl", target.clientWidth - 700, 500 - scroller_height]
+].forEach(([writing_mode, left, top]) => {
+ test(() => {
+ const target_left = getComputedStyle(target).left;
+ scroller.style.writingMode = writing_mode;
+ target.style.scrollSnapAlign = "start end";
+ if (writing_mode == "vertical-rl") {
+ target.style.left = (scroller_width - 700) + "px";
+ scroller.scrollTo(-500, 0);
+ } else {
+ scroller.scrollTo(0, 0);
+ }
+ assert_equals(scroller.scrollLeft, left, "aligns correctly on x");
+ assert_equals(scroller.scrollTop, top, "aligns correctly on y");
+ target.style.left = target_left;
+ scroller.style.writingMode = "";
+ }, "Snaps correctly for " + writing_mode +
+ " writing mode with 'scroll-snap-align: start end' alignment");
+});
+
+test(() => {
+ const target_left = getComputedStyle(target).left;
+ scroller.style.direction = "rtl";
+ target.style.scrollSnapAlign = "end start";
+ target.style.left = (scroller_width - 700) + "px";
+
+ scroller.scrollTo(-500, 0);
+ assert_equals(scroller.scrollLeft, target.clientWidth - 700,
+ "aligns correctly on x");
+ assert_equals(scroller.scrollTop, 500 - scroller_height,
+ "aligns correctly on y");
+
+ target.style.left = target_left;
+ scroller.style.direction = "";
+}, "Snaps correctly for 'direction: rtl' with 'scroll-snap-align: end start' " +
+ "alignment");
+
+test(() => {
+ const target_left = getComputedStyle(target).left;
+ scroller.style.direction = "rtl";
+ target.style.scrollSnapAlign = "start end";
+ target.style.left = (scroller_width - 700) + "px";
+
+ scroller.scrollTo(-500, 0);
+ assert_equals(scroller.scrollLeft, scroller_width - 700,
+ "aligns correctly on x");
+ assert_equals(scroller.scrollTop, 300, "aligns correctly on y");
+
+ target.style.left = target_left;
+ scroller.style.direction = "";
+}, "Snaps correctly for 'direction: rtl' with 'scroll-snap-align: start end' " +
+ "alignment");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-intended-direction.html b/testing/web-platform/tests/css/css-scroll-snap/snap-intended-direction.html
new file mode 100644
index 0000000000..4a1b56d251
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-intended-direction.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<title>`intended direction` scroll snaps only at points ahead of the scroll direction</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1766805">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+#scroller {
+ width: 200px;
+ height: 100px;
+ overflow: scroll;
+ scroll-snap-type: x mandatory;
+}
+.snap {
+ scroll-snap-align: start;
+ background: green;
+}
+</style>
+
+<div id="scroller">
+ <div style="width: 2000px; height: 100px;"></div>
+ <div class="snap" style="left: 0px; width: 20px; height: 100px;">1</div>
+ <div class="snap" style="left: 100px; width: 20px; height: 100px;">2</div>
+ <div class="snap" style="left: 120px; width: 20px; height: 100px;">3</div>
+ <div class="snap" style="left: 300px; width: 20px; height: 100px;">4</div>
+ <div class="snap" style="left: 400px; width: 20px; height: 100px;">5</div>
+</div>
+
+<script>
+test(() => {
+ scroller.scrollBy(10, 0);
+ assert_equals(scroller.scrollLeft, 100);
+
+ scroller.scrollBy(10, 0);
+ assert_equals(scroller.scrollLeft, 120);
+
+ scroller.scrollBy(10, 0);
+ // Snaps to the next snap point even if the previous snap point is closer to
+ // the current position.
+ assert_equals(scroller.scrollLeft, 300);
+}, "`intended direction` scroll snaps only at points ahead of the scroll " +
+ "direction");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-into-covering-area.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/snap-into-covering-area.tentative.html
new file mode 100644
index 0000000000..2cc8741f5e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-into-covering-area.tentative.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-actions.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ <script src="/dom/events/scrolling/scroll_support.js"></script>
+</head>
+
+<body>
+ <style>
+ #scroller {
+ overflow: scroll;
+ height: 500px;
+ width: 500px;
+ background-color: blue;
+ scroll-snap-type: y mandatory;
+ position: absolute;
+ }
+
+ .snap_point {
+ scroll-snap-align: start;
+ width: 40%;
+ position: relative;
+ left: 30%;
+ }
+
+ .big {
+ height: 1000%;
+ background-color: pink;
+ border: solid 1px red;
+ }
+
+ .small {
+ height: 50%;
+ background-color: purple;
+ border: solid 1px black;
+ }
+ </style>
+ <div id="scroller">
+ <div class="big snap_point" id="big_snap_point"></div>
+ <div class="small snap_point">
+ <button id="scrollerButton">scrollerButton</button>
+ </div>
+ </div>
+ <script>
+ promise_test(async(t) => {
+ const x = scroller.clientWidth / 2;
+ const y = scroller.clientHeight / 2;
+
+ // Scroll all the way down to the smaller snap area which doesn't cover
+ // the snapport.
+ let scrollend_promise = new Promise((resolve) => {
+ scroller.addEventListener("scrollend", resolve);
+ });
+ scroller.scrollTop = scroller.scrollHeight;
+ await scrollend_promise;
+
+ // Scroll up with one press of the arrow-up button.
+ scrollend_promise = new Promise((resolve) => {
+ scroller.addEventListener("scrollend", resolve);
+ });
+ const arrowUp = '\uE013';
+ await test_driver.send_keys(scrollerButton, arrowUp);
+
+ await scrollend_promise;
+ assert_equals(scroller.scrollTop, big_snap_point.offsetHeight - scroller.clientHeight,
+ "scroller is snapped to the bottom of the larger snap area, not the top");
+ });
+ </script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-on-focus.html b/testing/web-platform/tests/css/css-scroll-snap/snap-on-focus.html
new file mode 100644
index 0000000000..13709d2747
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-on-focus.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>Scroll snap on Element.focus()</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0;
+}
+
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow-y: scroll;
+ scroll-snap-type: y mandatory;
+}
+
+.snap {
+ width: 100%;
+ height: 300px;
+ top: 100px;
+ left: 0;
+ background-color: green;
+ scroll-snap-align: start none;
+}
+
+.no-snap {
+ width: 100%;
+ height: 300px;
+ top: 100px;
+ left: 0;
+ background-color: red;
+}
+
+.area {
+ width: 100%;
+ height: 2000px;
+}
+</style>
+
+<div id="scroller">
+ <div class="area"></div>
+ <div class="snap" style="top: 0px;"></div>
+ <div class="no-snap" style="top: 1000px;" tabindex=-1></div>
+ <div class="snap" style="top: 1200px;"></div>
+</div>
+
+<script>
+promise_test(async () => {
+ document.querySelector(".no-snap").focus();
+ await new Promise(resolve => step_timeout(resolve, 0));
+ assert_equals(scroller.scrollTop, 1200);
+}, "scroll snap should happens on Element.focus()");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-1.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-1.html
new file mode 100644
index 0000000000..985227bb53
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-1.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width">
+<title>
+ Snap to points of combinations of two different elements
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+#left-top {
+ /*
+ * the scroll position of this `scroll-snap-align: start start` is (200, 200)
+ */
+ left: 200px;
+ top: 200px;
+ height: 450px;
+ width: 450px;
+ background-color: rgb(255, 0, 0);
+ opacity: 0.5;
+ scroll-snap-align: start start;
+}
+
+#right-bottom {
+ /*
+ * the scroll position of this `scroll-snap-align: end end` is (50, 50),
+ * i.e, (`left` - scroll container's `width` + this element's `width,
+ `top` - scroll container's `height` + this element's `height)
+ */
+ left: 600px;
+ top: 600px;
+ width: 50px;
+ height: 50px;
+ background-color: rgb(0, 255, 0);
+ opacity: 0.5;
+ scroll-snap-align: end end;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="left-top"></div>
+ <div id="right-bottom"></div>
+</div>
+<script>
+test(t => {
+ const scroller = document.getElementById("scroller");
+ assert_equals(scroller.scrollLeft, 50);
+ assert_equals(scroller.scrollTop, 50);
+
+ scroller.scrollTo(100, 150);
+ assert_equals(scroller.scrollLeft, 50);
+ assert_equals(scroller.scrollTop, 200);
+
+ scroller.scrollTo(300, 300);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 200);
+
+ scroller.scrollTo(150, 100);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 50);
+}, 'Snap to points of combinations of two different elements');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-2.tentative.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-2.tentative.html
new file mode 100644
index 0000000000..8d06eb91d0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-combination-of-two-elements-2.tentative.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width">
+<title>
+ Snap to points of combinations of two different elements
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+#left-top {
+ /*
+ * the scroll position of this `scroll-snap-align: start start` is (200, 200)
+ */
+ left: 200px;
+ top: 200px;
+ height: 450px;
+ width: 450px;
+ background-color: rgb(255, 0, 0);
+ opacity: 0.5;
+ scroll-snap-align: start start;
+}
+
+#right-bottom {
+ /*
+ * the scroll position of this `scroll-snap-align: end end` is (50, 250),
+ * i.e, (`left` - scroll container's `width` + this element's `width,
+ `top` - scroll container's `height` + this element's `height)
+ */
+ left: 600px;
+ top: 800px;
+ width: 50px;
+ height: 50px;
+ background-color: rgb(0, 255, 0);
+ opacity: 0.5;
+ scroll-snap-align: end end;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="left-top"></div>
+ <div id="right-bottom"></div>
+</div>
+<script>
+test(t => {
+ // There are four combinations of snap positions defined by #top-left and
+ // #right-bottom elements.
+ // (200, 200), (50, 250), (200, 250) and (50, 200).
+ // But snapping to (50, 200) leaves the snap area of #right-bottom element
+ // outside of the snapport, thus it won't be a valid snap position.
+ const scroller = document.getElementById("scroller");
+
+ // The nearest valid snap position from (0, 0) is (50, 250).
+ assert_equals(scroller.scrollLeft, 50);
+ assert_equals(scroller.scrollTop, 250);
+
+ // (50, 200) is not valid, thus the nearest one from (100, 150) is (200, 200).
+ scroller.scrollTo(100, 150);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 200);
+
+ scroller.scrollTo(300, 300);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 250);
+
+ scroller.scrollTo(200, 100);
+ assert_equals(scroller.scrollLeft, 200);
+ assert_equals(scroller.scrollTop, 200);
+}, 'Snap to points of combinations of two different elements');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-transformed-target.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-transformed-target.html
new file mode 100644
index 0000000000..b8604269b4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-transformed-target.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+}
+#scroller {
+ overflow: hidden; /* TODO: Use scrollbar-width: none */
+ scroll-snap-type: x mandatory;
+ width: 500px;
+ height: 500px;
+}
+.space {
+ width: 2000px;
+ height: 2000px;
+}
+#target {
+ height: 200px;
+ width: 200px;
+ left: 50px;
+ background-color: blue;
+}
+</style>
+<div id="scroller">
+ <div class="space"></div>
+ <div id="target"></div>
+</div>
+<script>
+test(() => {
+ target.style.scrollSnapAlign = "start";
+ target.style.transform = "translateX(300px)";
+ scroller.scrollTo(10, 0);
+ assert_equals(scroller.scrollLeft, 350 /* left + translateX(300px) */);
+ assert_equals(scroller.scrollTop, 0);
+}, "Snaps to the transformed snap start position");
+
+test(() => {
+ target.style.scrollSnapAlign = "end";
+ target.style.transform = "translateX(300px)";
+ scroller.scrollTo(10, 0);
+ assert_equals(scroller.scrollLeft,
+ 50 /* left + width + translateX(300px) - scroller.width */);
+ assert_equals(scroller.scrollTop, 0);
+}, "Snaps to the transformed snap end position");
+
+test(() => {
+ target.style.scrollSnapAlign = "start";
+ target.style.transform = "translateX(-100px)";
+ scroller.scrollTo(10, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+}, "Snaps to visible top left position of the transformed box");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-both.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-both.html
new file mode 100644
index 0000000000..cde329bcc3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-both.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<title>
+ Snap to a visible area only even when there is a closer snap point for an area
+ that is closer but not visible (using both axes snap type)
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-scope"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap {
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#left-top {
+ left: 0px;
+ top: 0px;
+}
+
+#left-bottom {
+ left: 0px;
+ top: 800px;
+}
+
+#right-top {
+ left: 800px;
+ top: 0px;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="left-top" class="snap"></div>
+ <div id="left-bottom" class="snap"></div>
+ <div id="right-top" class="snap"></div>
+</div>
+<script>
+test(t => {
+ const scroller = document.getElementById("scroller");
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+ scroller.scrollTo(500, 600);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 800);
+ scroller.scrollTo(600, 500);
+ assert_equals(scroller.scrollLeft, 800);
+ assert_equals(scroller.scrollTop, 0);
+}, 'Only snap to visible areas in the case where taking the closest snap point of \
+ each axis does not snap to a visible area');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-both.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-both.html
new file mode 100644
index 0000000000..4c3c3c11a6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-both.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<title>
+ Snap to an area where the element's scroll-margin is visible but not the
+ element itself (using both axes snap type)
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-scope"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: scroll;
+ scroll-snap-type: both mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap {
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#left-top {
+ left: 0px;
+ top: 0px;
+}
+
+#left-bottom {
+ left: 0px;
+ top: 800px;
+ /* 800px scroll-margin makes the snap area span to the right end of the
+ right-top area */
+ scroll-margin-right: 800px;
+}
+
+#right-top {
+ left: 800px;
+ top: 0px;
+ /* 800px scroll-margin makes the snap area span to the bottom end of the
+ left-bottom area */
+ scroll-margin-bottom: 800px;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="left-top" class="snap"></div>
+ <div id="left-bottom" class="snap"></div>
+ <div id="right-top" class="snap"></div>
+</div>
+<script>
+test(() => {
+ const scroller = document.getElementById("scroller");
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+ // 750 and 650 are picked as those are closer to top left of the intersection
+ // (800, 800) of the snap areas where the browser should snap. This makes the
+ // intersection a closer snap option than a covering option that the browser
+ // might choose where the snapport is aligned on the bottom and right.
+ scroller.scrollTo(650, 750);
+ assert_equals(scroller.scrollLeft, 800);
+ assert_equals(scroller.scrollTop, 800);
+ scroller.scrollTo(750, 650);
+ assert_equals(scroller.scrollLeft, 800);
+ assert_equals(scroller.scrollTop, 800);
+}, 'Snap to area such that only the scroll margin from both axes\' areas are \
+visible');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.html
new file mode 100644
index 0000000000..dea9225a47
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-x-axis.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>
+ Snap to an area where the element's scroll-margin is visible but not the
+ element itself (using x-axis snap type)
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-scope"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: scroll;
+ scroll-snap-type: x mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap {
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#left-visible {
+ left: 0px;
+ top: 0px;
+}
+
+#right-visible {
+ left: 800px;
+ top: 0px;
+}
+
+#middle-margin-visible {
+ left: 700px;
+ top: 800px;
+ /* 300px makes snap area visible with margin but non-visible without it */
+ scroll-margin-top: 300px;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="left-visible" class="snap"></div>
+ <div id="middle-margin-visible" class="snap"></div>
+ <div id="right-visible" class="snap"></div>
+</div>
+<script>
+test(() => {
+ const scroller = document.getElementById("scroller");
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+ scroller.scrollTo(500, 0);
+ assert_equals(scroller.scrollLeft, 700);
+ assert_equals(scroller.scrollTop, 0);
+}, 'Scroll margin should be considered when calculating snap area visibilty \
+while snapping on the x-axis');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.html
new file mode 100644
index 0000000000..60c5488445
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-margin-y-axis.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<title>
+ Snap to an area where the element's scroll-margin is visible but not the
+ element itself (using y-axis snap type)
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-scope"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap {
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#top-visible {
+ left: 0px;
+ top: 0px;
+}
+
+#bottom-visible {
+ left: 0px;
+ top: 800px;
+}
+
+#middle-margin-visible {
+ left: 800px;
+ top: 700px;
+ /* 300px makes snap area visible with margin but non-visible without it */
+ scroll-margin-left: 300px;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="top-visible" class="snap"></div>
+ <div id="middle-margin-visible" class="snap"></div>
+ <div id="bottom-visible" class="snap"></div>
+</div>
+<script>
+test(() => {
+ const scroller = document.getElementById("scroller");
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+ scroller.scrollTo(0, 500);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 700);
+}, 'Scroll margin should be considered when calculating snap area visibilty \
+while snapping on the y-axis');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-x-axis.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-x-axis.html
new file mode 100644
index 0000000000..b8b226f1af
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-x-axis.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>
+ Snap to a visible area only even when there is a closer snap point for an area
+ that is closer but not visible (using x-axis snap type)
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-scope"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: scroll;
+ scroll-snap-type: x mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap {
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#left-visible {
+ left: 0px;
+ top: 0px;
+}
+
+#right-visible {
+ left: 800px;
+ top: 0px;
+}
+
+#middle-not-visible {
+ left: 700px;
+ top: 800px;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="left-visible" class="snap"></div>
+ <div id="middle-not-visible" class="snap"></div>
+ <div id="right-visible" class="snap"></div>
+</div>
+<script>
+test(() => {
+ const scroller = document.getElementById("scroller");
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+ scroller.scrollTo(500, 0);
+ assert_equals(scroller.scrollLeft, 800);
+ assert_equals(scroller.scrollTop, 0);
+}, 'Only snap to visible area on X axis, even when the non-visible ones are closer');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-y-axis.html b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-y-axis.html
new file mode 100644
index 0000000000..80d2e9946d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/snap-to-visible-areas-y-axis.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<title>
+ Snap to a visible area only even when there is a closer snap point for an area
+ that is closer but not visible (using y-axis snap type)
+</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#snap-scope"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+
+#scroller {
+ height: 600px;
+ width: 600px;
+ overflow: scroll;
+ scroll-snap-type: y mandatory;
+}
+
+#space {
+ width: 2000px;
+ height: 2000px;
+}
+
+.snap {
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+
+#top-visible {
+ left: 0px;
+ top: 0px;
+}
+
+#bottom-visible {
+ left: 0px;
+ top: 800px;
+}
+
+#middle-not-visible {
+ left: 800px;
+ top: 700px;
+}
+
+</style>
+<div id="scroller">
+ <div id="space"></div>
+ <div id="top-visible" class="snap"></div>
+ <div id="middle-not-visible" class="snap"></div>
+ <div id="bottom-visible" class="snap"></div>
+</div>
+<script>
+test(() => {
+ const scroller = document.getElementById("scroller");
+ scroller.scrollTo(0, 0);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+ scroller.scrollTo(0, 500);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 800);
+}, 'Only snap to visible area on Y axis, even when the non-visible ones are closer');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/support/common.css b/testing/web-platform/tests/css/css-scroll-snap/support/common.css
new file mode 100644
index 0000000000..f49c7cbacd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/support/common.css
@@ -0,0 +1,44 @@
+body {
+ margin: 0;
+}
+
+#scroller {
+ position: absolute;
+ width: 400px;
+ height: 400px;
+ overflow: scroll;
+ padding: 0;
+
+ scroll-snap-type: both mandatory;
+}
+
+.snap {
+ position: absolute;
+ width: 200px;
+ height: 200px;
+ background-color: blue;
+
+ scroll-snap-align: start;
+}
+
+#space {
+ position: absolute;
+ width: 1000px;
+ height: 1000px;
+}
+
+.left {
+ left: 0;
+}
+
+.top {
+ top: 0;
+}
+
+.right {
+ left: 400px;
+}
+
+.bottom {
+ top: 400px;
+} \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-scroll-snap/support/common.js b/testing/web-platform/tests/css/css-scroll-snap/support/common.js
new file mode 100644
index 0000000000..c7800b95f1
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/support/common.js
@@ -0,0 +1,131 @@
+const KEY_CODE_MAP = {
+ 'ArrowLeft': '\uE012',
+ 'ArrowUp': '\uE013',
+ 'ArrowRight': '\uE014',
+ 'ArrowDown': '\uE015',
+ 'PageUp': '\uE00E',
+ 'PageDown': '\uE00F',
+ 'End': '\uE010',
+ 'Home': '\uE011',
+ 'Space': ' ',
+};
+
+// Send key event to the target element using test driver. Supports human
+// friendly key names for common keyboard scroll operations e.g., arrow keys,
+// page keys, etc.
+async function keyPress(target, key) {
+ let code = key;
+ if (KEY_CODE_MAP.hasOwnProperty(key))
+ code = KEY_CODE_MAP[key];
+
+ // First move pointer on target and click to ensure it receives the key.
+ return test_driver.send_keys(target, code);
+}
+
+// Use rAF to wait for the value of the getter function passed to not change for
+// at least 15 frames or timeout after 1 second.
+//
+// Example usage:
+// await waitForAnimationEnd(() => scroller.scrollTop);
+function waitForAnimationEnd(getValue) {
+ const TIMEOUT = 1000; // milliseconds
+ const MAX_UNCHANGED_FRAMES = 15;
+
+ const start_time = performance.now();
+ let last_changed_frame = 0;
+ let last_value = getValue();
+
+ return new Promise((resolve, reject) => {
+ function tick(frames, time) {
+ // We requestAnimationFrame either for TIMEOUT milliseconds or until
+ // MAX_UNCHANGED_FRAMES with no change have been observed.
+ if (time - start_time > TIMEOUT ||
+ frames - last_changed_frame >= MAX_UNCHANGED_FRAMES) {
+ resolve(time);
+ } else {
+ current_value = getValue();
+ if (last_value != current_value) {
+ last_changed_frame = frames;
+ last_value = current_value;
+ }
+ requestAnimationFrame(tick.bind(this, frames + 1));
+ }
+ }
+ tick(0, start_time);
+ });
+}
+
+
+function waitForEvent(eventTarget, type) {
+ return new Promise(resolve => {
+ eventTarget.addEventListener(type, resolve, { once: true });
+ });
+}
+
+function waitForScrollEvent(eventTarget) {
+ return waitForEvent(eventTarget, 'scroll');
+}
+
+function waitForWheelEvent(eventTarget) {
+ return waitForEvent(eventTarget, 'wheel');
+}
+
+function waitForScrollStop(eventTarget) {
+ const TIMEOUT_IN_MS = 200;
+
+ return new Promise(resolve => {
+ let lastScrollEventTime = performance.now();
+
+ const scrollListener = () => {
+ lastScrollEventTime = performance.now();
+ };
+ eventTarget.addEventListener('scroll', scrollListener);
+
+ const tick = () => {
+ if (performance.now() - lastScrollEventTime > TIMEOUT_IN_MS) {
+ eventTarget.removeEventListener('scroll', scrollListener);
+ resolve();
+ return;
+ }
+ requestAnimationFrame(tick); // wait another frame
+ }
+ requestAnimationFrame(tick);
+ });
+}
+
+function waitForScrollEnd(eventTarget) {
+ if (window.onscrollend !== undefined) {
+ return waitForScrollendEventNoTimeout(eventTarget);
+ }
+ return waitForScrollEvent(eventTarget).then(() => {
+ return waitForScrollStop(eventTarget);
+ });
+}
+
+function waitForScrollTo(eventTarget, getValue, targetValue) {
+ return new Promise((resolve, reject) => {
+ const scrollListener = (evt) => {
+ if (getValue() == targetValue) {
+ eventTarget.removeEventListener('scroll', scrollListener);
+ resolve(evt);
+ }
+ };
+ if (getValue() == targetValue)
+ resolve();
+ else
+ eventTarget.addEventListener('scroll', scrollListener);
+ });
+}
+
+function waitForNextFrame() {
+ return new Promise(resolve => {
+ const start = performance.now();
+ requestAnimationFrame(frameTime => {
+ if (frameTime < start) {
+ requestAnimationFrame(resolve);
+ } else {
+ resolve();
+ }
+ });
+ });
+}
diff --git a/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-align-001-iframe.html b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-align-001-iframe.html
new file mode 100644
index 0000000000..d86a5e86d0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-align-001-iframe.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<title>iframe for #target and snap position with snapping off</title>
+<style type='text/css'>
+ html, body {
+ margin: 0; padding: 0;
+ }
+ html {
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x fixed;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ div {
+ height: 1em;
+ }
+ html { scroll-padding: .5em 0 0; } /* set up a snap position */
+ #target { scroll-margin: .5em 0 0;
+ scroll-snap-align: center; }
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div class="fail">FAIL</div>
+<div></div>
+<div id="stripe"></div>
+<div id="target"></div>
+<div></div>
+<div class="fail">FAIL</div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-margin-001-iframe.html b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-margin-001-iframe.html
new file mode 100644
index 0000000000..2b2c1d2d8c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-margin-001-iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>iframe for #target and scroll-margin with snapping off (y</title>
+<style type='text/css'>
+ html, body {
+ margin: 0; padding: 0;
+ }
+ html {
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x fixed;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ div {
+ height: 1em;
+ }
+ #target { scroll-margin: 2em 0 1em; } /* snap area is exact fit for snapport */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div class="fail">FAIL</div>
+<div></div>
+<div id="stripe"></div>
+<div id="target"></div>
+<div></div>
+<div class="fail">FAIL</div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-padding-001-iframe.html b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-padding-001-iframe.html
new file mode 100644
index 0000000000..9260c81b1c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-padding-001-iframe.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>iframe for #target and scroll-snap-padding with snapping off (y</title>
+<style type='text/css'>
+ html, body {
+ margin: 0; padding: 0;
+ }
+ html {
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x fixed;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+ }
+ div {
+ height: 1em;
+ }
+ html { scroll-padding: 2em 0 1em; } /* snap area is exact fit for snapport */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div class="fail">FAIL</div>
+<div></div>
+<div id="stripe"></div>
+<div id="target"></div>
+<div></div>
+<div class="fail">FAIL</div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-snap-001-iframe.html b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-snap-001-iframe.html
new file mode 100644
index 0000000000..b67c3f8d3e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/support/scroll-target-snap-001-iframe.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<title>iframe for #target and snap position with snapping on</title>
+<style type='text/css'>
+ html, body {
+ margin: 0; padding: 0;
+ }
+ html {
+ /* to make failing more obvious */
+ background: 0 1em / 100% 1em linear-gradient(red, red) repeat-x fixed;
+ /* avoid anti-aliasing issues */
+ font: 20px/1 sans-serif;
+ scrollbar-width: none;
+
+ /* turn on snapping */
+ scroll-snap-type: block;
+ }
+ div {
+ height: 1em;
+ }
+ /* Note: we use "start" for #target to avoid spec ambiguity where
+ * scroll-snap-align conflicts with default ScrollIntoViewOptions.
+ * See https://github.com/w3c/csswg-drafts/issues/9576.
+ */
+ #target { scroll-margin: 2em 0 0;
+ scroll-snap-align: start; } /* set up a snap position */
+ #stripe { background: green; } /* color part of the snap area */
+ .fail { color: red; } /* make failing more obvious */
+
+ /* Try to foil the UA */
+ .foilup { margin-bottom: -1em; scroll-snap-align: start; }
+ .foildn { margin-top: -1em; scroll-snap-align: end; }
+
+ /* emulate `scrollbar-width: none` for browsers that don't support it yet */
+ ::-webkit-scrollbar { display: none; }
+</style>
+
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div class="foilup"></div>
+<div class="fail">FAIL</div>
+<div></div>
+<div id="stripe"></div>
+<div class="foilup"></div>
+<div id="target"></div>
+<div class="foildn"></div>
+<div></div>
+<div class="fail">FAIL</div>
+<div class="foildn"></div>
+<div></div>
+<div class="foildn"></div>
+<div></div>
+<div></div>
+<div></div>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-001.html b/testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-001.html
new file mode 100644
index 0000000000..ca4f6033ce
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-001.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#unreachable" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+div {
+ position: absolute;
+ margin: 0px;
+}
+#scroller {
+ height: 500px;
+ width: 500px;
+ overflow: hidden;
+ scroll-snap-type: both mandatory;
+}
+#unreachable {
+ width: 300px;
+ height: 300px;
+ top: -100px;
+ left: -100px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+#reachable {
+ width: 300px;
+ height: 300px;
+ top: 400px;
+ left: 400px;
+ background-color: blue;
+ scroll-snap-align: start;
+}
+</style>
+
+<div id="scroller">
+ <div style="width: 2000px; height: 2000px;"></div>
+ <div id="unreachable"></div>
+ <div id="reachable"></div>
+</div>
+
+<script>
+test(() => {
+ // Firstly move to the reachable snap position.
+ scroller.scrollTo(400, 400);
+ assert_equals(scroller.scrollLeft, 400);
+ assert_equals(scroller.scrollTop, 400);
+
+ // Then move to a position between the unreachable snap position and the
+ // reachable position but closer to the unreachable one.
+ scroller.scrollTo(100, 100);
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+}, "Snaps to the positions defined by the element as much as possible");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-002.html b/testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-002.html
new file mode 100644
index 0000000000..a3726d71b3
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-snap/unreachable-snap-positions-002.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap/#unreachable" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+.scroller {
+ width: 100vw;
+ height: 100px;
+ display: flex;
+ scroll-snap-type: x mandatory;
+ overflow-x: auto;
+}
+.scroller.rtl {
+ direction: rtl;
+}
+.scroller.end > span {
+ scroll-snap-align: end;
+}
+.scroller.center > span {
+ scroll-snap-align: end;
+}
+</style>
+<div class="scroller end">
+ <span style="min-width: 25px;"></span>
+ <span style="min-width: 100vw;"></span>
+</div>
+<div class="scroller center">
+ <span style="min-width: 25px;"></span>
+ <span style="min-width: 100vw;"></span>
+</div>
+<div class="scroller end rtl">
+ <span style="min-width: 25px;"></span>
+ <span style="min-width: 100vw;"></span>
+</div>
+<div class="scroller center rtl">
+ <span style="min-width: 25px;"></span>
+ <span style="min-width: 100vw;"></span>
+</div>
+<script>
+
+test(() => {
+ const scroller = document.querySelector(".scroller.end");
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+}, "Unreachable snap point with `scroll-snap-align: end`");
+
+test(() => {
+ const scroller = document.querySelector(".scroller.center");
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+}, "Unreachable snap point with `scroll-snap-align: center`");
+
+test(() => {
+ const scroller = document.querySelector(".scroller.end.rtl");
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+}, "Unreachable snap point with `scroll-snap-align: end` in RTL");
+
+test(() => {
+ const scroller = document.querySelector(".scroller.center.rtl");
+ assert_equals(scroller.scrollLeft, 0);
+ assert_equals(scroller.scrollTop, 0);
+}, "Unreachable snap point with `scroll-snap-align: center` in RTL");
+
+</script>
+</html>