summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/css/css-scroll-anchoring
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-anchoring
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-anchoring')
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/META.yml3
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/README.md8
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html55
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html40
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-001.html38
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-002.html43
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-003.html39
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/adjustment-followed-by-scrollBy.html62
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/adjustments-in-scroll-event-handler.tentative.html53
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-001.html44
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-002.html39
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-003.html39
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-004.html45
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/ancestor-change-heuristic.html81
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/anchor-inside-iframe.html27
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html51
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html38
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html28
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/anonymous-block-box.html34
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/basic.html23
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/clamp-negative-overflow.html61
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/clipped-scrollers-skipped.html38
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/contain-paint-offscreen-container.html42
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-float.html36
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-overflow.html30
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/device-pixel-adjustment.html77
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/dirty-contents-reselect-anchor.tentative.html54
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/exclude-fixed-position.html26
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/exclude-inline.html34
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/exclude-sticky.html28
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/focus-prioritized.html46
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/focused-element-in-excluded-subtree.html63
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/focused-element-nested-anchor.html69
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/focused-element-outside-scroller.html50
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/fragment-scrolling-anchors.html60
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/fullscreen-crash.html33
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update-from-scroll-event-listener.html59
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update.html58
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/history-restore-anchors.html36
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/image-001.html29
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/infinite-scroll-event.tentative.html42
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/inheritance.html21
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/inline-block-002.html28
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/inline-block.html26
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/multicol-fragmented-anchor.html56
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/negative-layout-overflow.html44
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-ref.html44
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical-ref.html45
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical.html52
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout.html51
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic-scroller.html49
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic.html51
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/opt-out-inner-table.html47
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/opt-out-table.html45
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/opt-out.html74
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-computed.html19
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-invalid.html18
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-valid.html18
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-change.html72
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-to-abspos-change.html71
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-ib-split.html57
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-in-nested-scroll-box.html85
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic.html82
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/reading-scroll-forces-anchoring.html30
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/scroll-padding-affects-anchoring.html32
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html141
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/subtree-exclusion.html45
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/support/flexbox-scrolling-vertical-rl.html20
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/support/history-restore-anchors-new-window.html29
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/support/scrolling-vertical-rl.html18
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/table-collapsed-borders-crash.html25
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/text-anchor-in-vertical-rl.html30
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-000.html21
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-001.html21
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/wrapped-text.html28
-rw-r--r--testing/web-platform/tests/css/css-scroll-anchoring/zero-scroll-offset.html53
76 files changed, 3309 insertions, 0 deletions
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/META.yml b/testing/web-platform/tests/css/css-scroll-anchoring/META.yml
new file mode 100644
index 0000000000..3d24f5cc9f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/META.yml
@@ -0,0 +1,3 @@
+spec: https://drafts.csswg.org/css-scroll-anchoring/
+suggested_reviewers:
+ - tabatkins
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/README.md b/testing/web-platform/tests/css/css-scroll-anchoring/README.md
new file mode 100644
index 0000000000..78f1387bdf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/README.md
@@ -0,0 +1,8 @@
+## Scroll Anchoring Test Suite
+
+Scroll anchoring adjusts the scroll position to prevent visible jumps (or
+"reflows") when content changes above the viewport.
+
+* [explainer](https://github.com/WICG/ScrollAnchoring/blob/master/explainer.md)
+* [spec](https://drafts.csswg.org/css-scroll-anchoring/)
+* [file bug / view open issues](https://github.com/w3c/csswg-drafts/issues)
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html
new file mode 100644
index 0000000000..76a4952383
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { margin: 0; }
+#scroller { overflow: scroll; width: 500px; height: 400px; }
+#space { height: 1000px; }
+#abs {
+ position: absolute; background-color: red;
+ width: 100px; height: 100px;
+ left: 25px; top: 25px;
+}
+#rel {
+ position: relative; background-color: green;
+ left: 50px; top: 100px; width: 100px; height: 75px;
+}
+
+</style>
+<div id="scroller">
+ <div id="space">
+ <div id="abs"></div>
+ <div id="before"></div>
+ <div id="rel"></div>
+ </div>
+</div>
+<script>
+
+// Tests that anchor node selection skips an absolute-positioned descendant of
+// the scroller if and only if its containing block is outside the scroller.
+
+test(() => {
+ var scroller = document.querySelector("#scroller");
+ var abs = document.querySelector("#abs");
+ var before = document.querySelector("#before");
+ var rel = document.querySelector("#rel");
+
+ // We should not anchor to #abs, because it does not move with the scroller.
+ scroller.scrollTop = 25;
+ before.style.height = "25px";
+ assert_equals(scroller.scrollTop, 50);
+
+ // Reset, make #scroller a containing block.
+ before.style.height = "0";
+ scroller.scrollTop = 0;
+ scroller.style.position = "relative";
+
+ // This time we should anchor to #abs.
+ scroller.scrollTop = 25;
+ before.style.height = "25px";
+ assert_equals(scroller.scrollTop, 25);
+
+}, "Abs-pos descendant with containing block outside the scroller.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html
new file mode 100644
index 0000000000..5d8ff9a911
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body, html, #static { height: 0; }
+#abs {
+ position: absolute;
+ left: 50px;
+ top: 50px;
+ height: 1200px;
+ padding: 50px;
+ border: 5px solid gray;
+}
+#anchor {
+ background-color: #afa;
+ width: 100px;
+ height: 100px;
+}
+
+</style>
+<div id="static">
+ <div id="abs">
+ <div id="changer"></div>
+ <div id="anchor"></div>
+ </div>
+</div>
+<script>
+
+// Tests that the "bounds" of an element, for the purpose of visibility in the
+// anchor node selection algorithm, include any space occupied by the element's
+// positioned descendants.
+
+test(() => {
+ document.scrollingElement.scrollTop = 120;
+ document.querySelector("#changer").style.height = "100px";
+ assert_equals(document.scrollingElement.scrollTop, 220);
+}, "Abs-pos with zero-height static parent.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-001.html b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-001.html
new file mode 100644
index 0000000000..ee11148747
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-001.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com" />
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#candidate-examination">
+<meta name="assert" content="The candidate examination algorithm iterates abspos descendants of the containing block. Being inside a fragmentainer shouldn't break that.">
+<style>
+html {
+ column-count: 1; /* Fragmentainer */
+}
+
+body {
+ position: relative; /* Containing block */
+}
+
+div {
+ position: absolute; /* Abspos */
+ font-size: 100px;
+ width: 200px;
+ height: 4000px;
+ line-height: 100px;
+}
+</style>
+<div>abc <b id=b>def</b> ghi</div>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Tests anchoring to a text node that is moved by preceding text,
+// everything in an absolutely positioned element whose containing block
+// is inside a multi-column fragmentainer.
+
+test(() => {
+ var b = document.querySelector("#b");
+ var preText = b.previousSibling;
+ document.scrollingElement.scrollTop = 150;
+ preText.nodeValue = "abcd efg ";
+ assert_equals(document.scrollingElement.scrollTop, 250);
+}, "Anchoring with text wrapping changes.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-002.html b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-002.html
new file mode 100644
index 0000000000..6c254d437a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-002.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com" />
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#candidate-examination">
+<meta name="assert" content="The candidate examination algorithm iterates abspos descendants of the containing block. Being inside a fully clipped element and inside a fragmentainer shouldn't break that.">
+<style>
+html {
+ column-count: 1; /* Fragmentainer */
+}
+
+body {
+ position: relative; /* Containing block */
+ height: 4000px;
+}
+
+main {
+ height: 0px; /* Fully clipped */
+}
+
+div {
+ position: absolute; /* Abspos */
+ font-size: 100px;
+ width: 200px;
+ height: 100%;
+ line-height: 100px;
+}
+</style>
+<main><div>abc <b id=b>def</b> ghi</div></main>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Tests anchoring to a text node that is moved by preceding text,
+// where the text is inside an absolutely positioned element,
+// there is a fully clippped element between the abspos and its contaning block,
+// and the containing block is inside a multi-column fragmentainer.
+test(() => {
+ var b = document.querySelector("#b");
+ var preText = b.previousSibling;
+ document.scrollingElement.scrollTop = 150;
+ preText.nodeValue = "abcd efg ";
+ assert_equals(document.scrollingElement.scrollTop, 250);
+}, "Anchoring with text wrapping changes.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-003.html b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-003.html
new file mode 100644
index 0000000000..01caa895da
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-003.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="author" title="Oriol Brufau" href="mailto:obrufau@igalia.com" />
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org" />
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#candidate-examination">
+<meta name="assert" content="The candidate examination algorithm iterates abspos descendants of the containing block. Being inside a fragmentainer shouldn't break that.">
+<style>
+html {
+ column-count: 1; /* Fragmentainer */
+}
+
+main {
+ position: relative; /* Containing block */
+}
+
+div {
+ position: absolute; /* Abspos */
+ font-size: 100px;
+ width: 200px;
+ height: 4000px;
+ line-height: 100px;
+}
+</style>
+<main><div>abc <b id=b>def</b> ghi</div></main>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Tests anchoring to a text node that is moved by preceding text,
+// everything in an absolutely positioned element whose containing block
+// is inside a multi-column fragmentainer.
+
+test(() => {
+ var b = document.querySelector("#b");
+ var preText = b.previousSibling;
+ document.scrollingElement.scrollTop = 150;
+ preText.nodeValue = "abcd efg ";
+ assert_equals(document.scrollingElement.scrollTop, 250);
+}, "Anchoring with text wrapping changes.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/adjustment-followed-by-scrollBy.html b/testing/web-platform/tests/css/css-scroll-anchoring/adjustment-followed-by-scrollBy.html
new file mode 100644
index 0000000000..7428147b83
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/adjustment-followed-by-scrollBy.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1874551">
+<style>
+div {
+ height: round(20vh, 1px);
+}
+</style>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div id="center" style="height: 100px; background-color: blue;"></div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<div></div>
+<script>
+promise_test(async t => {
+ assert_equals(window.scrollY, 0);
+
+ // Center an element.
+ center.scrollIntoView({ block: "center" }, { behavior: "instant" });
+ await new Promise(resolve => window.addEventListener("scroll", resolve));
+
+ const originalPosition = center.getBoundingClientRect().top;
+
+ // Given that now the element is centered, one of div elements above the
+ // centered element is used as the scroll anchor node. Hide two div elements,
+ // the first div and the div element just above the centered one to move the
+ // anchor node, thus it will trigger scroll anchoring adjustment.
+ center.previousElementSibling.style.display = "none";
+ document.querySelectorAll("div")[0].style.display = "none";
+
+ // And adjust the scroll position where the centered element is still
+ // positioned at the center of the scroll container.
+ window.scrollBy(0, center.getBoundingClientRect().top - originalPosition);
+ await new Promise(resolve => window.addEventListener("scroll", resolve));
+
+ const centeredPosition = center.getBoundingClientRect().top;
+
+ // Now try to scrollIntoView({ block: "center" }) and make sure the position
+ // is unchanged.
+ center.scrollIntoView({ block: "center" }, { behavior: "instant" });
+
+ // Wait two frames to be able to scroll if it's possible.
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ assert_equals(centeredPosition, center.getBoundingClientRect().top);
+}, "Scroll anchoring followed by scrollBy call");
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/adjustments-in-scroll-event-handler.tentative.html b/testing/web-platform/tests/css/css-scroll-anchoring/adjustments-in-scroll-event-handler.tentative.html
new file mode 100644
index 0000000000..84fd79cbcb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/adjustments-in-scroll-event-handler.tentative.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<style>
+ body { margin: 0 }
+ .content {
+ height: 200px;
+ background: lightblue;
+ }
+ .spacer {
+ height: 300vh;
+ }
+</style>
+<div class="content"></div>
+<div class="content" style="background: green"></div>
+<div class="spacer"></div>
+<script>
+const anchor = document.querySelectorAll(".content")[1];
+
+const t = async_test("Scroll adjustments happen even if it's triggered from scroll event listeners");
+window.addEventListener("scroll", t.step_func(function() {
+ // Forcibly flush layout, this will flush the pending the node insertion.
+ let scrollPosition = window.scrollY;
+
+ requestAnimationFrame(t.step_func(function() {
+ requestAnimationFrame(t.step_func(function() {
+ assert_equals(window.scrollY, 400);
+ t.done();
+ }));
+ }));
+}), { once: true });
+
+window.onload = t.step_func(function() {
+ requestAnimationFrame(t.step_func(function() {
+ // Scroll to the anchor node in a requestAnimationFrame callback so that
+ // it queues a scroll event which will be fired in the next event loop.
+ anchor.scrollIntoView({ behavior: "instant" });
+
+ // Then in a setTimeout callback insert an element just right before the
+ // anchor node, it will run before firing the scroll event.
+ t.step_timeout(function() {
+ const content = document.createElement("div");
+ content.classList.add("content");
+ content.style.background = "red";
+ anchor.before(content);
+ }, 0);
+ }));
+});
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-001.html b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-001.html
new file mode 100644
index 0000000000..e155c6625c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-001.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1852818">
+<style>
+div {
+ height: 100vh;
+}
+</style>
+<div></div>
+<div id="anchor" style="height: 10px; background-color: blue;"></div>
+<div></div>
+<script>
+promise_test(async t => {
+ assert_equals(window.scrollY, 0);
+
+ let anchorRect = anchor.getBoundingClientRect();
+ // Scroll to the anchor node.
+ window.scrollBy(0, anchorRect.y);
+ assert_equals(window.scrollY, anchorRect.y);
+
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+
+ // Expand the first element so that scroll anchoring happens.
+ document.querySelectorAll("div")[0].style.height = "200vh";
+ // Flush the change.
+ document.querySelectorAll("div")[0].getBoundingClientRect();
+
+ await new Promise(resolve => window.addEventListener("scroll", resolve));
+
+ // Revert the height change in a scroll event handler.
+ document.querySelectorAll("div")[0].style.height = "";
+
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ assert_equals(window.scrollY, anchorRect.y);
+}, "Scroll anchoring properly works after scrollable range shrinkage");
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-002.html b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-002.html
new file mode 100644
index 0000000000..d6399c5a6c
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-002.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1779404">
+<div style="height: 500vh;"></div>
+<div id="anchor" style="height: 10px; background-color: blue;"></div>
+<div style="height: 200vh;"></div>
+<script>
+promise_test(async t => {
+ assert_equals(window.scrollY, 0);
+
+ let anchorRect = anchor.getBoundingClientRect();
+ // Scroll to the anchor node.
+ window.scrollBy(0, anchorRect.y);
+ assert_equals(window.scrollY, anchorRect.y);
+
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+
+ // Shrink the first element so that scroll anchoring happens.
+ document.querySelectorAll("div")[0].style.height = "100vh";
+
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ const scrollAnchorPosition = window.scrollY;
+
+ // Scroll back to (0, 0) to calculate the expected scroll anchor element
+ // position.
+ window.scrollTo(0, 0);
+ anchorRect = anchor.getBoundingClientRect();
+ assert_equals(scrollAnchorPosition, anchorRect.y);
+}, "Scroll anchoring properly works after scrollable range shrinkage");
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-003.html b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-003.html
new file mode 100644
index 0000000000..c9127e79b5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-003.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1856088">
+<div style="height: 500vh;"></div>
+<div id="anchor" style="height: 10px; background-color: blue;"></div>
+<div style="height: 200vh;"></div>
+<script>
+promise_test(async t => {
+ assert_equals(window.scrollY, 0);
+
+ let anchorRect = anchor.getBoundingClientRect();
+ // Scroll to the anchor node.
+ window.scrollBy(0, anchorRect.y);
+ assert_equals(window.scrollY, anchorRect.y);
+
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+
+ // Shrink the first element so that scroll anchoring happens.
+ document.querySelectorAll("div")[0].style.height = "200vh";
+ // Flush the change.
+ document.querySelectorAll("div")[0].getBoundingClientRect();
+
+ await new Promise(resolve => window.addEventListener("scroll", resolve));
+
+ // Revert the height change in a scroll event handler.
+ document.querySelectorAll("div")[0].style.height = "500vh";
+
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ assert_equals(window.scrollY, anchorRect.y);
+}, "Scroll anchoring properly works after scrollable range shrinkage");
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-004.html b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-004.html
new file mode 100644
index 0000000000..8509a1cca5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/after-scrollable-range-shrinkage-004.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<meta name="viewport" content="width=device-width">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1856088">
+<div style="height: 500vh;"></div>
+<div id="anchor" style="height: 10px; background-color: blue;"></div>
+<div style="height: 200vh;"></div>
+<script>
+promise_test(async t => {
+ assert_equals(window.scrollY, 0);
+
+ let anchorRect = anchor.getBoundingClientRect();
+ // Scroll to the anchor node.
+ window.scrollBy(0, anchorRect.y);
+ assert_equals(window.scrollY, anchorRect.y);
+
+ await new Promise(resolve => t.step_timeout(resolve, 0));
+
+ // Shrink the first element so that scroll anchoring happens.
+ document.querySelectorAll("div")[0].style.height = "200vh";
+ // Flush the change.
+ document.querySelectorAll("div")[0].getBoundingClientRect();
+
+ await new Promise(resolve => window.addEventListener("scroll", resolve));
+
+ // Shrink the first element more.
+ document.querySelectorAll("div")[0].style.height = "100vh";
+
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ const scrollAnchorPosition = window.scrollY;
+
+ // Scroll back to (0, 0) to calculate the expected scroll anchor element
+ // position.
+ window.scrollTo(0, 0);
+ anchorRect = anchor.getBoundingClientRect();
+ assert_equals(scrollAnchorPosition, anchorRect.y);
+}, "Scroll anchoring properly works after scrollable range shrinkage");
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/ancestor-change-heuristic.html b/testing/web-platform/tests/css/css-scroll-anchoring/ancestor-change-heuristic.html
new file mode 100644
index 0000000000..21adfbb6b7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/ancestor-change-heuristic.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#space { height: 4000px; }
+#ancestor { position: relative; }
+#before, #anchor { height: 100px; }
+#anchor { background-color: green; }
+
+.layout1 { padding-top: 20px; }
+.layout2 { margin-right: 20px; }
+.layout3 { max-width: 100px; }
+.layout4 { min-height: 400px; }
+.layout5 { position: static !important; }
+.layout6 { left: 20px; }
+.layout7 { transform: matrix(1, 0, 0, 1, 50, 50); }
+.nonLayout1 { color: red; }
+.nonLayout2 { font-size: 200%; }
+.nonLayout3 { box-shadow: 10px 10px 10px gray; }
+.nonLayout4 { opacity: 0.5; }
+.nonLayout5 { z-index: -1; }
+
+.scroller {
+ overflow: scroll;
+ width: 600px;
+ height: 600px;
+}
+
+</style>
+<div id="maybeScroller">
+ <div id="space">
+ <div id="ancestor">
+ <div id="before"></div>
+ <div id="anchor"></div>
+ </div>
+ </div>
+</div>
+<script>
+
+// Tests that scroll anchoring is suppressed when one of the "layout-affecting"
+// properties is modified on an ancestor of the anchor node.
+
+var scroller;
+var ancestor = document.querySelector("#ancestor");
+var before = document.querySelector("#before");
+
+function runCase(classToApply, expectSuppression) {
+ // Reset.
+ scroller.scrollTop = 0;
+ ancestor.className = "";
+ before.style.height = "";
+ scroller.scrollTop = 150;
+
+ ancestor.className = classToApply;
+ before.style.height = "150px";
+
+ var expectedTop = expectSuppression ? 150 : 200;
+ assert_equals(scroller.scrollTop, expectedTop);
+}
+
+function runAll() {
+ for (var i = 1; i <= 7; i++)
+ runCase("layout" + i, true);
+ for (var i = 1; i <= 5; i++)
+ runCase("nonLayout" + i, false);
+}
+
+test(() => {
+ document.querySelector("#maybeScroller").className = "";
+ scroller = document.scrollingElement;
+ runAll();
+}, "Ancestor changes in document scroller.");
+
+test(() => {
+ scroller = document.querySelector("#maybeScroller");
+ scroller.className = "scroller";
+ runAll();
+}, "Ancestor changes in scrollable <div>.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/anchor-inside-iframe.html b/testing/web-platform/tests/css/css-scroll-anchoring/anchor-inside-iframe.html
new file mode 100644
index 0000000000..ea1ce4b13d
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/anchor-inside-iframe.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<iframe width="700" height="500" srcdoc="
+ <!DOCTYPE html>
+ <style> body { height: 1000px } div { height: 100px } </style>
+ <div id='block1'>abc</div>
+ <div id='block2'>def</div>
+"></iframe>
+<script>
+ async_test((t) => {
+ var iframeWindow = document.querySelector("iframe").contentWindow;
+ iframeWindow.addEventListener("load", () => {
+ var block1 = iframeWindow.document.querySelector("#block1");
+ iframeWindow.scrollTo(0, 150);
+
+ requestAnimationFrame(() => {
+ step_timeout(() => {
+ block1.style.height = "200px";
+ assert_equals(iframeWindow.scrollY, 250);
+ t.done();
+ }, 0);
+ });
+ });
+ }, "Scroll anchoring in an iframe.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html b/testing/web-platform/tests/css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html
new file mode 100644
index 0000000000..7f0c54d1dc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+ height: 200px;
+ width: 200px;
+ overflow: scroll;
+}
+#a1, #space1, #a2, #space2 {
+ height: 200px;
+}
+#a1, #a2 {
+ background-color: #8f8;
+}
+
+</style>
+<div id="scroller">
+ <div id="space0"></div>
+ <div id="a1"></div>
+ <div id="space1"></div>
+ <div id="a2"></div>
+ <div id="space2"></div>
+</div>
+<script>
+
+// Tests that the anchor node is recomputed after an explicit change to the
+// scroll position.
+
+test(() => {
+ var scroller = document.querySelector("#scroller");
+ scroller.scrollTop = 500;
+
+ // We should now be anchored to #a2.
+ document.querySelector("#space1").style.height = "300px";
+ assert_equals(scroller.scrollTop, 600);
+
+ scroller.scrollTop = 100;
+
+ // We should now be anchored to #a1. Make sure there is no adjustment from
+ // moving #a2.
+ document.querySelector("#space1").style.height = "400px";
+ assert_equals(scroller.scrollTop, 100);
+
+ // Moving #a1 should produce an adjustment.
+ document.querySelector("#space0").style.height = "100px";
+ assert_equals(scroller.scrollTop, 200);
+}, "Anchor node recomputed after an explicit scroll occurs.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html b/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html
new file mode 100644
index 0000000000..3de725e683
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+ height: 500px;
+ width: 200px;
+ overflow: scroll;
+}
+#changer { height: 1500px; }
+#anchor {
+ width: 150px;
+ height: 1000px;
+ overflow: scroll;
+}
+
+</style>
+<div id="scroller">
+ <div id="changer"></div>
+ <div id="anchor"></div>
+</div>
+<script>
+
+// Test that scroll anchoring interacts correctly with scroll bounds clamping
+// inside a scrollable <div> element.
+//
+// There should be no visible jump even if the content shrinks such that the
+// new max scroll position is less than the previous scroll position.
+
+test(() => {
+ var scroller = document.querySelector("#scroller");
+ scroller.scrollTop = 1600;
+ document.querySelector("#changer").style.height = "0";
+ assert_equals(scroller.scrollTop, 100);
+}, "Anchoring combined with scroll bounds clamping in a <div>.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html b/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html
new file mode 100644
index 0000000000..e9d06579d5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#changer { height: 1500px; }
+#anchor {
+ width: 150px;
+ height: 4000px;
+ background-color: pink;
+}
+
+</style>
+<div id="changer"></div>
+<div id="anchor"></div>
+<script>
+
+// Test that scroll anchoring interacts correctly with scroll bounds clamping:
+// There should be no visible jump even if the content shrinks such that the
+// new max scroll position is less than the previous scroll position.
+
+test(() => {
+ document.scrollingElement.scrollTop = 1600;
+ document.querySelector("#changer").style.height = "0";
+ assert_equals(document.scrollingElement.scrollTop, 100);
+}, "Anchoring combined with scroll bounds clamping in the document.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/anonymous-block-box.html b/testing/web-platform/tests/css/css-scroll-anchoring/anonymous-block-box.html
new file mode 100644
index 0000000000..97542e2613
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/anonymous-block-box.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 2000px; margin: 0 10px; }
+#before, #after { height: 100px; }
+#before { margin-bottom: 100px; }
+#container { line-height: 100px; vertical-align: top; }
+
+</style>
+<div id="container">
+ <div id="before">before</div>
+ <span id="inline">inline</span>
+ <div id="after">after</div>
+</div>
+<script>
+
+// Tests anchoring inside an anonymous block box. The anchor selection algorithm
+// should descend into the children of the anonymous box even though it is fully
+// contained in the viewport.
+
+test(() => {
+ document.scrollingElement.scrollTop = 150;
+
+ var span = document.querySelector("#inline");
+ var newSpan = document.createElement("span");
+ newSpan.innerHTML = "inserted<br>";
+ span.parentNode.insertBefore(newSpan, span);
+
+ assert_equals(document.scrollingElement.scrollTop, 250);
+}, "Anchor selection descent into anonymous block boxes.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/basic.html b/testing/web-platform/tests/css/css-scroll-anchoring/basic.html
new file mode 100644
index 0000000000..99625ba7da
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/basic.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px; }
+div { height: 100px; }
+
+</style>
+<div id="block1">abc</div>
+<div id="block2">def</div>
+<script>
+
+// Tests that growing an element above the viewport produces a scroll
+// anchoring adjustment equal to the amount by which it grew.
+
+test(() => {
+ document.scrollingElement.scrollTop = 150;
+ document.querySelector("#block1").style.height = "200px";
+ assert_equals(document.scrollingElement.scrollTop, 250);
+}, "Minimal scroll anchoring example.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/clamp-negative-overflow.html b/testing/web-platform/tests/css/css-scroll-anchoring/clamp-negative-overflow.html
new file mode 100644
index 0000000000..4f0b5fe9a2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/clamp-negative-overflow.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style type="text/css">
+ #scroller {
+ overflow: scroll;
+ width: 500px;
+ height: 500px;
+ }
+ #anchor {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ margin-top: 100px;
+ margin-bottom: 1000px;
+ background-color: blue;
+ }
+ #positioned {
+ position: absolute;
+ width: 10px;
+ height: 10px;
+ top: -200px;
+ background-color: yellow;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div id="anchor">
+ <div id="positioned">
+ </div>
+ </div>
+ </div>
+ <script type="text/javascript">
+ test(() => {
+ let scroller = document.querySelector('#scroller');
+ let positioned = document.querySelector('#positioned');
+
+ // Scroll down to select #anchor as an anchor node
+ scroller.scrollTop = 20;
+
+ // Move #positioned downwards, which will move the unclamped scrollable
+ // overflow rect of #anchor downards as well
+ positioned.style.top = '-180px';
+ // To trigger the bug that this regression tests in Gecko, we need
+ // to not take Gecko's relative positioning fast path. To do
+ // this, change the 'left' of #positioned from 'auto' to '0px'.
+ positioned.style.left = '0px';
+
+ // The implementation should clamp the scrollable overflow rect
+ // before the start-edge of the anchor node, and not apply an
+ // adjustment
+ assert_equals(scroller.scrollTop, 20);
+ }, 'scrollable overflow before the start-edge of the anchor node should be clamped');
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/clipped-scrollers-skipped.html b/testing/web-platform/tests/css/css-scroll-anchoring/clipped-scrollers-skipped.html
new file mode 100644
index 0000000000..594cd604f4
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/clipped-scrollers-skipped.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 2000px; }
+#scroller { overflow: scroll; width: 500px; height: 300px; }
+.anchor {
+ position: relative; height: 100px; width: 150px;
+ background-color: #afa; border: 1px solid gray;
+}
+#forceScrolling { height: 500px; background-color: #fcc; }
+
+</style>
+<div id="scroller">
+ <div id="innerChanger"></div>
+ <div id="innerAnchor" class="anchor"></div>
+ <div id="forceScrolling"></div>
+</div>
+<div id="outerChanger"></div>
+<div id="outerAnchor" class="anchor"></div>
+<script>
+
+// Test that we ignore the clipped content when computing visibility otherwise
+// we may end up with an anchor that we think is in the viewport but is not.
+
+test(() => {
+ document.querySelector("#scroller").scrollTop = 100;
+ document.scrollingElement.scrollTop = 350;
+
+ document.querySelector("#innerChanger").style.height = "200px";
+ document.querySelector("#outerChanger").style.height = "150px";
+
+ assert_equals(document.querySelector("#scroller").scrollTop, 300);
+ assert_equals(document.scrollingElement.scrollTop, 500);
+}, "Anchor selection with nested scrollers.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/contain-paint-offscreen-container.html b/testing/web-platform/tests/css/css-scroll-anchoring/contain-paint-offscreen-container.html
new file mode 100644
index 0000000000..58f41cc748
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/contain-paint-offscreen-container.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<link rel="author" title="Vladimir Levin" href="mailto:vmpstr@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<meta name="assert" content="ensures that scroll anchoring does not recurse into contained offscreen elements">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+body { height: 10000px; }
+#container { contain: paint; }
+#overflow {
+ position: relative;
+ top: 300px;
+ height: 10px;
+}
+#anchor {
+ width: 10px;
+ height: 50px;
+}
+</style>
+
+<div style="height: 800px"></div>
+<div id="container" style="height: 40px">
+ <div id=overflow></div>
+</div>
+<div id="changer" style="height: 150px"></div>
+<div id=anchor></div>
+
+<script>
+test(() => {
+ // Ensure #anchor is the only thing on screen.
+ // Note that #overflow would be on screen if container
+ // did not have layout and paint containment.
+ document.scrollingElement.scrollTop = 1000;
+
+ // Ensure anchor doesn't move if #changer shrinks.
+ const offset = anchor.getBoundingClientRect().y;
+ document.querySelector("#changer").style.height = "50px";
+ assert_equals(anchor.getBoundingClientRect().y, offset);
+}, "Contain: style paint container offscreen.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-float.html b/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-float.html
new file mode 100644
index 0000000000..ff39608ff0
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-float.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px; }
+#outer { width: 300px; }
+#outer:after { content: " "; clear:both; display: table; }
+#float {
+ float: left; background-color: #ccc;
+ height: 500px; width: 100%;
+}
+#inner { height: 100px; background-color: green; }
+
+</style>
+<div id="outer">
+ <div id="zeroheight">
+ <div id="float">
+ <div id="changer"></div>
+ <div id="inner"></div>
+ </div>
+ </div>
+</div>
+<div>after</div>
+<script>
+
+// Tests that we descend into zero-height containers that have floating content.
+
+test(() => {
+ document.scrollingElement.scrollTop = 50;
+ assert_equals(document.querySelector("#zeroheight").offsetHeight, 0);
+ document.querySelector("#changer").style.height = "50px";
+ assert_equals(document.scrollingElement.scrollTop, 100);
+}, "Zero-height container with float.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-overflow.html b/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-overflow.html
new file mode 100644
index 0000000000..654f34a051
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-overflow.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px; }
+#outer { width: 300px; }
+#zeroheight { height: 0px; }
+#changer { height: 100px; background-color: red; }
+#bottom { margin-top: 600px; }
+
+</style>
+<div id="outer">
+ <div id="zeroheight">
+ <div id="changer"></div>
+ <div id="bottom">bottom</div>
+ </div>
+</div>
+<script>
+
+// Tests that the anchor selection algorithm descends into zero-height
+// containers that have overflowing content.
+
+test(() => {
+ document.scrollingElement.scrollTop = 200;
+ document.querySelector("#changer").style.height = "200px";
+ assert_equals(document.scrollingElement.scrollTop, 300);
+}, "Zero-height container with visible overflow.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/device-pixel-adjustment.html b/testing/web-platform/tests/css/css-scroll-anchoring/device-pixel-adjustment.html
new file mode 100644
index 0000000000..4a135939fd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/device-pixel-adjustment.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body {
+ height: 200vh;
+}
+#anchor {
+ width: 100px;
+ height: 100px;
+ background-color: blue;
+}
+
+</style>
+<div id="expander"></div>
+<div id="anchor"></div>
+<script>
+
+// This tests that scroll anchor adjustments can happen by quantities smaller
+// than a device pixel.
+//
+// Unfortunately, we can't test this by simply reading 'scrollTop', because
+// 'scrollTop' may be rounded to the nearest CSS pixel. So, to test that
+// subpixel adjustments can in fact happen, we repeatedly trigger a scroll
+// adjustment in a way that would produce a different final .scrollTop value,
+// depending on whether or not we rounded each adjustment as we apply it.
+
+test(() => {
+ let scroller = document.scrollingElement;
+ let expander = document.querySelector("#expander");
+ let anchor = document.querySelector("#anchor");
+ const initialTop = 10;
+
+ // Scroll 10px to activate scroll anchoring
+ scroller.scrollTop = initialTop;
+
+ // Helper to insert a div with specified height before the anchor node
+ function addChild(height) {
+ let child = document.createElement("div");
+ child.style.height = `${height}px`;
+ anchor.before(child);
+ }
+
+ // Calculate what fraction of a CSS pixel corresponds to one device pixel
+ let devicePixel = 1.0 / window.devicePixelRatio;
+ assert_true(devicePixel <= 1.0, "there should be more device pixels than CSS pixels");
+
+ // The 0.5 is an arbitrary scale when creating the subpixel delta
+ let delta = 0.5 * devicePixel;
+
+ // To help us check for for premature rounding of adjustments, we'll
+ // trigger "count" subpixel adjustments of size "delta", where "count" is
+ // the first positive integer such that:
+ // round(count * delta) != count * round(delta)
+ // As round(X) and count are integers, this happens when:
+ // count * delta = count * round(delta) +/- 1
+ // Solving for count:
+ // count = 1 / abs(delta - round(delta))
+ // Note that we don't need to worry about the denominator being zero, as:
+ // 0 < devicePixel <= 1
+ // And so halving devicePixel should never yield a whole number.
+ let count = 1 / Math.abs(delta - Math.round(delta));
+
+ for (let i = 0; i < count; i++) {
+ addChild(delta);
+ // Trigger an anchor adjustment by forcing a layout flush
+ scroller.scrollTop;
+ }
+
+ let destination = Math.round(initialTop + delta * count);
+ assert_equals(scroller.scrollTop, destination,
+ `adjusting by ${delta}px, ${count} times, should be the same as adjusting by ${delta * count}px, once.`);
+}, "Test that scroll anchor adjustments can happen by a sub device-pixel amount.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/dirty-contents-reselect-anchor.tentative.html b/testing/web-platform/tests/css/css-scroll-anchoring/dirty-contents-reselect-anchor.tentative.html
new file mode 100644
index 0000000000..41adf53a0f
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/dirty-contents-reselect-anchor.tentative.html
@@ -0,0 +1,54 @@
+<!doctype html>
+<meta charset=utf-8>
+<link rel=help href="https://bugzilla.mozilla.org/show_bug.cgi?id=1738781">
+<link rel=help href="https://github.com/w3c/csswg-drafts/issues/6787">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ .padding {
+ background: grey;
+ border: 1px dashed black;
+ margin: 5px;
+ height: 200vh;
+ }
+</style>
+<div id="content"></div>
+<script>
+ const content = document.getElementById("content");
+
+ const t = async_test("Scroll anchor is re-selected after adjustment if there are dirty descendants at selection time");
+ function replaceAllContent() {
+ content.innerHTML = `
+ <div class="padding"></div>
+ <button id="target">Scroll target</button>
+ <div class="padding"></div>
+ `;
+ }
+
+ function insertContent() {
+ let inserted = document.createElement("div");
+ inserted.className = "padding inserted";
+ content.insertBefore(inserted, content.firstChild);
+ }
+
+ // Set the content, and scroll #target into view.
+ replaceAllContent();
+ document.getElementById("target").scrollIntoView();
+
+ t.step(function() {
+ assert_not_equals(window.scrollY, 0, "Should've scrolled");
+ });
+
+ // Save the target scroll position, which shouldn't change.
+ const oldTargetTop = document.getElementById("target").getBoundingClientRect().top;
+
+ // Replace all the content, then insert content at the top afterwards.
+ replaceAllContent();
+
+ requestAnimationFrame(() => requestAnimationFrame(t.step_func_done(function() {
+ insertContent();
+ const newTargetTop = document.getElementById("target").getBoundingClientRect().top;
+ assert_equals(oldTargetTop, newTargetTop, "Scroll position should've been preserved");
+ })));
+</script>
+<style>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/exclude-fixed-position.html b/testing/web-platform/tests/css/css-scroll-anchoring/exclude-fixed-position.html
new file mode 100644
index 0000000000..d48d3f7ced
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/exclude-fixed-position.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px; margin: 0; }
+#fixed, #content { width: 200px; height: 100px; }
+#fixed { position: fixed; left: 100px; top: 50px; }
+#before { height: 50px; }
+#content { margin-top: 100px; }
+
+</style>
+<div id="fixed">fixed</div>
+<div id="before"></div>
+<div id="content">content</div>
+<script>
+
+// Tests that the anchor selection algorithm skips fixed-positioned elements.
+
+test(() => {
+ document.scrollingElement.scrollTop = 100;
+ document.querySelector("#before").style.height = "100px";
+ assert_equals(document.scrollingElement.scrollTop, 150);
+}, "Fixed-position header.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/exclude-inline.html b/testing/web-platform/tests/css/css-scroll-anchoring/exclude-inline.html
new file mode 100644
index 0000000000..cea6b61dfe
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/exclude-inline.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#expander {
+ margin-bottom: 50px;
+}
+#no {
+ overflow-anchor: none;
+}
+#spacing {
+ margin-bottom: 300vh;
+}
+
+</style>
+<span>out of view</span>
+<div id="expander"></div>
+<span id="no">excluded subtree <span>[nested inline]</span></span>
+<div id="spacing"></div>
+<script>
+
+// Tests that an inline element can be an excluded subtree.
+
+test(() => {
+ scrollTo(0, 50);
+ document.querySelector('#expander').style = "margin-bottom: 100px";
+ assert_equals(document.scrollingElement.scrollTop, 50,
+ "Scroll anchoring should not anchor within the span.");
+ scrollTo(0, 0);
+});
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/exclude-sticky.html b/testing/web-platform/tests/css/css-scroll-anchoring/exclude-sticky.html
new file mode 100644
index 0000000000..2158d39802
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/exclude-sticky.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 400vh; margin: 0; }
+#sticky, #content { width: 200px; height: 100px; }
+#sticky { position: sticky; left: 100px; top: 50px; }
+#before { height: 50px; }
+#content { margin-top: 100px; }
+
+</style>
+<div id="sticky">sticky</div>
+<div id="before"></div>
+<div id="content">content</div>
+<script>
+
+// Tests that the anchor selection algorithm skips sticky-positioned elements.
+
+test(() => {
+ document.scrollingElement.scrollTop = 150;
+ document.querySelector("#before").style.height = "100px";
+ assert_equals(document.scrollingElement.scrollTop, 200);
+}, "Sticky-positioned headers shouldn't be chosen as scroll anchors (we should use 'content' instead)");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/focus-prioritized.html b/testing/web-platform/tests/css/css-scroll-anchoring/focus-prioritized.html
new file mode 100644
index 0000000000..32d90badca
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/focus-prioritized.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<title>CSS Scroll Anchoring: prioritize focused element</title>
+<link rel="author" title="Vladimir Levin" href="mailto:vmpstr@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#anchor-node-selection">
+<meta name="assert" content="anchor selection prioritized focused element">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+body { height: 4000px }
+.spacer { height: 100px }
+#growing { height: 100px }
+#focused { height: 10px }
+</style>
+
+<div class=spacer></div>
+<div class=spacer></div>
+<div class=spacer></div>
+<div class=spacer></div>
+<div id=growing></div>
+<div class=spacer></div>
+<div id=focused contenteditable></div>
+<div class=spacer></div>
+<div class=spacer></div>
+
+<script>
+async_test((t) => {
+ document.scrollingElement.scrollTop = 150;
+ focused.focus();
+
+ const target_rect = focused.getBoundingClientRect();
+ growing.style.height = "3000px";
+
+ requestAnimationFrame(() => {
+ t.step(() => {
+ const new_rect = focused.getBoundingClientRect();
+ assert_equals(new_rect.x, target_rect.x, "x coordinate");
+ assert_equals(new_rect.y, target_rect.y, "y coordinate");
+ assert_not_equals(document.scrollingElement.scrollTop, 150, "scroll adjusted");
+ });
+ t.done();
+ });
+}, "Anchor selection prioritized focused element.");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-in-excluded-subtree.html b/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-in-excluded-subtree.html
new file mode 100644
index 0000000000..85b3107802
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-in-excluded-subtree.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px; }
+div { height: 100px; }
+
+.scroller {
+ overflow: scroll;
+ position: fixed;
+ width: 300px;
+ height: 300px;
+ background-color: green;
+}
+
+#posSticky {
+ top: 300px;
+ position: relative;
+ height: 50px;
+ width: 50px;
+ background-color: blue;
+}
+
+#content {
+ background-color: #D3D3D3;
+ height: 50px;
+ width: 50px;
+ position: relative;
+ top: 500px;
+}
+
+</style>
+<div id="scroller" class="scroller">
+ <div id="content"></div>
+
+ <div id="posSticky">
+ <div id="block1" tabindex="-1">abc</div>
+ </div>
+</div>
+
+<script>
+
+// Tests that a focused element doesn't become the
+// priority candidate of the main frame if it is
+// in an excluded subtree
+
+promise_test(async function() {
+ var scroller = document.querySelector("#scroller");
+ var focusElement = document.querySelector("#block1");
+ focusElement.focus();
+ scroller.scrollBy(0,150);
+ document.scrollingElement.scrollBy(0,100);
+
+ await new Promise(resolve => {
+ document.addEventListener("scroll", () => step_timeout(resolve, 0));
+ });
+
+ assert_equals(document.scrollingElement.scrollTop, 100);
+}, "Ensure there is no scroll anchoring adjustment in the main frame.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-nested-anchor.html b/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-nested-anchor.html
new file mode 100644
index 0000000000..727da4046e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-nested-anchor.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px; }
+div { height: 100px; }
+
+.scroller {
+ overflow: scroll;
+ position: fixed;
+ width: 300px;
+ height: 300px;
+ background-color: green;
+}
+
+#posSticky {
+ top: 300px;
+ position: relative;
+ height: 50px;
+ width: 50px;
+ background-color: blue;
+}
+
+#content {
+ background-color: #D3D3D3;
+ height: 50px;
+ width: 50px;
+ position: relative;
+ top: 500px;
+}
+
+#anchor {
+ background-color: brown;
+ height: 50px;
+ width: 50px;
+ position: relative;
+ top: 200px;
+}
+
+</style>
+<div id="scroller" class="scroller">
+ <div id="content" tabindex="-1"></div>
+ <div id="anchor" tabindex="-1"></div>
+</div>
+
+<script>
+
+// Tests that a focused element doesn't become the
+// priority candidate of the main frame if it is
+// the anchor element of a subscroller
+
+promise_test(async function() {
+ var scroller = document.querySelector("#scroller");
+ var focusElement = document.querySelector("#anchor");
+ focusElement.focus();
+ scroller.scrollBy(0,150);
+ document.scrollingElement.scrollBy(0,100);
+
+ scroller.scrollBy(0, 50);
+ await new Promise(resolve => {
+ scroller.addEventListener("scroll", () => step_timeout(resolve, 0));
+ });
+
+ assert_equals(document.scrollingElement.scrollTop, 100);
+}, "Ensure there is no scroll anchoring adjustment in the main frame.");
+
+</script> \ No newline at end of file
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-outside-scroller.html b/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-outside-scroller.html
new file mode 100644
index 0000000000..73a5944856
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/focused-element-outside-scroller.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px; }
+div { height: 100px; }
+
+.scroller {
+ overflow: scroll;
+ position: fixed;
+ width: 300px;
+ height: 300px;
+ background-color: green;
+}
+
+#content {
+ background-color: #D3D3D3;
+ height: 50px;
+ width: 50px;
+ position: relative;
+ top: 500px;
+}
+
+</style>
+<div id="scroller" class="scroller">
+ <div id="content"></div>
+</div>
+<div id="block1" tabindex="-1">abc</div>
+
+<script>
+
+// Tests that a focused element doesn't become the
+// priority candidate of the subscroller
+
+promise_test(async function() {
+ var scroller = document.querySelector("#scroller");
+ var focusElement = document.querySelector("#block1");
+ focusElement.focus();
+ scroller.scrollBy(0,150);
+ document.scrollingElement.scrollBy(0,100);
+ await new Promise(resolve => {
+ document.addEventListener("scroll", () => step_timeout(resolve, 0));
+ });
+
+ assert_equals(scroller.scrollTop, 150);
+}, "Ensure there is no scroll anchoring adjustment in subscroller.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/fragment-scrolling-anchors.html b/testing/web-platform/tests/css/css-scroll-anchoring/fragment-scrolling-anchors.html
new file mode 100644
index 0000000000..99c679acaa
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/fragment-scrolling-anchors.html
@@ -0,0 +1,60 @@
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ body {
+ margin: 0px;
+ height: 2000px;
+ width: 2000px;
+ }
+
+ #first {
+ height: 1000px;
+ background-color: #FFA5D2;
+ }
+
+ #anchor {
+ position: absolute;
+ background-color: #84BE6A;
+ height: 600px;
+ width: 100%;
+ }
+
+ #fragment {
+ position: relative;
+ background-color: orange;
+ height: 200px;
+ width: 200px;
+ margin: 10px;
+ }
+</style>
+
+<div id="first"></div>
+<div id="changer"></div>
+<div id="anchor">
+ <div id="fragment" name="fragment"></div>
+</div>
+
+<script>
+ promise_test(async function(t) {
+ // Note that this test passes even without scroll anchoring because of
+ // fragment anchoring.
+ window.location.hash = 'fragment';
+ // Height of first + fragment margin-top.
+ assert_equals(window.scrollY, 1010);
+
+ // Change height of content above fragment.
+ var ch = document.getElementById('changer');
+ ch.style.height = 100;
+
+ await new Promise(resolve => addEventListener('scroll', resolve, {once: true}));
+
+ // Height of first + height changer + fragment margin-top.
+ assert_equals(window.scrollY, 1110);
+
+ first.remove();
+ changer.remove();
+ anchor.remove();
+ }, 'Verify scroll anchoring interaction with fragment scrolls');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/fullscreen-crash.html b/testing/web-platform/tests/css/css-scroll-anchoring/fullscreen-crash.html
new file mode 100644
index 0000000000..545f2919b5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/fullscreen-crash.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=823150">
+
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<style>
+
+#a { height: 700px; }
+#b { border: 4px solid #ccc; }
+
+</style>
+<div id="a"><div id="b"></div></div>
+<script>
+
+onload = () => {
+ test_driver.bless("requestFullscreen", step2);
+};
+step2 = () => {
+ b.requestFullscreen();
+ b.addEventListener('fullscreenchange', step3);
+};
+step3 = () => {
+ document.designMode = "on";
+ document.execCommand("selectAll");
+ document.execCommand("formatBlock", false, "p");
+ document.documentElement.classList.remove('test-wait');
+};
+
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update-from-scroll-event-listener.html b/testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update-from-scroll-event-listener.html
new file mode 100644
index 0000000000..b3964dfcc7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update-from-scroll-event-listener.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1586909">
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #scroller {
+ overflow: scroll;
+ height: 500px;
+ height: 500px;
+ }
+ #before {
+ height: 200px;
+ }
+ #anchor {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ margin-bottom: 500px;
+ background-color: blue;
+ top: 0px;
+ }
+</style>
+<div id="scroller">
+ <div id="before">
+ </div>
+ <div id="anchor">
+ </div>
+</div>
+<script>
+async_test(t => {
+ let scroller = document.querySelector('#scroller');
+ let before = document.querySelector('#before');
+ let anchor = document.querySelector('#anchor');
+
+ scroller.onscroll = t.step_func(function() {
+ // Adjust the 'top' of #anchor, which should trigger a suppression
+ anchor.style.top = '10px';
+
+ // Expand #before and make sure we don't apply an adjustment
+ before.style.height = '300px';
+
+ assert_equals(scroller.scrollTop, 200);
+
+ t.step_timeout(t.step_func_done(function() {
+ // Expand #before again and make sure we don't keep #anchor as
+ // an anchor from the last time.
+ before.style.height = '600px';
+ assert_equals(scroller.scrollTop, 200);
+ }), 0);
+ });
+
+ // Scroll down to select #anchor as a scroll anchor
+ scroller.scrollTop = 200;
+}, 'Positioned ancestors with dynamic changes to offsets trigger scroll suppressions.');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update.html b/testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update.html
new file mode 100644
index 0000000000..7fcbd983ed
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<html>
+<head>
+ <style type="text/css">
+ #scroller {
+ overflow: scroll;
+ height: 500px;
+ height: 500px;
+ }
+ #before {
+ height: 200px;
+ }
+ #anchor {
+ position: relative;
+ width: 200px;
+ height: 200px;
+ margin-bottom: 500px;
+ background-color: blue;
+ /*
+ * To trigger the Gecko bug that's being regression-tested here, we
+ * need 'top' to start out at a non-'auto' value, so that the
+ * dynamic change can trigger Gecko's "RecomputePosition" fast path
+ */
+ top: 0px;
+ }
+ </style>
+</head>
+<body>
+ <div id="scroller">
+ <div id="before">
+ </div>
+ <div id="anchor">
+ </div>
+ </div>
+
+ <script type="text/javascript">
+ test(() => {
+ let scroller = document.querySelector('#scroller');
+ let before = document.querySelector('#before');
+ let anchor = document.querySelector('#anchor');
+
+ // Scroll down to select #anchor as a scroll anchor
+ scroller.scrollTop = 200;
+
+ // Adjust the 'top' of #anchor, which should trigger a suppression
+ anchor.style.top = '10px';
+
+ // Expand #before and make sure we don't apply an adjustment
+ before.style.height = '300px';
+ assert_equals(scroller.scrollTop, 200);
+ }, 'Positioned ancestors with dynamic changes to offsets trigger scroll suppressions.');
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/history-restore-anchors.html b/testing/web-platform/tests/css/css-scroll-anchoring/history-restore-anchors.html
new file mode 100644
index 0000000000..ecd7806bc9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/history-restore-anchors.html
@@ -0,0 +1,36 @@
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+ var win;
+ var messageCount = 0;
+ // Navigation steps:
+ // 1- page gets loaded and anchor element gets scrolled into view.
+ // 2- loaded page refreshed.
+ async_test(function(t) {
+ window.onmessage = function() {
+ if (++messageCount == 1) {
+ t.step(() => {
+ var anchor = win.document.getElementById('anchor');
+ anchor.scrollIntoView();
+ assert_equals(win.scrollY, 1000);
+ win.location.reload();
+ });
+ } else {
+ t.step(() => {
+ assert_equals(win.scrollY, 1000);
+ // Change height of content above anchor.
+ var ch = win.document.getElementById('changer');
+ ch.style.height = 100;
+ // Height of first + height changer.
+ assert_equals(win.scrollY, 1100)
+ t.done();
+ });
+ win.close();
+ }
+ };
+ win = window.open('support/history-restore-anchors-new-window.html');
+ }, 'Verify scroll anchoring interaction with history restoration');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/image-001.html b/testing/web-platform/tests/css/css-scroll-anchoring/image-001.html
new file mode 100644
index 0000000000..475c9170b8
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/image-001.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Scroll Anchoring: Anchor node can be an image</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#anchor-node-selection">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/4247">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1540203">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+body { height: 4000px }
+#spacer { height: 100px }
+img {
+ width: 500px;
+ height: 500px;
+ background: green;
+}
+</style>
+<div id=spacer></div>
+<br><br>
+<img>
+<script>
+test(() => {
+ document.scrollingElement.scrollTop = 150;
+ document.querySelector("#spacer").style.height = "150px";
+ assert_equals(document.scrollingElement.scrollTop, 200);
+}, "Anchor selection can select images");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/infinite-scroll-event.tentative.html b/testing/web-platform/tests/css/css-scroll-anchoring/infinite-scroll-event.tentative.html
new file mode 100644
index 0000000000..e2a2998c52
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/infinite-scroll-event.tentative.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1561450">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers">
+<style>
+ body { margin: 0 }
+ .content {
+ height: 45vh;
+ background: lightblue;
+ }
+</style>
+<div class="content"></div>
+<div id="hidden" style="display: none; height: 200px"></div>
+<div class="content"></div>
+<div class="content"></div>
+<div class="content"></div>
+<script>
+let count = 0;
+const t = async_test("Scroll adjustments don't keep happening with 0-length adjustments triggered by a single scroll operation");
+onscroll = t.step_func(function() {
+ ++count;
+ hidden.style.display = "block";
+ hidden.offsetTop;
+ hidden.style.display = "none";
+ let currentCount = count;
+ requestAnimationFrame(t.step_func(function() {
+ requestAnimationFrame(t.step_func(function() {
+ if (currentCount == count) {
+ t.done();
+ }
+ }));
+ }));
+});
+
+window.onload = t.step_func(function() {
+ window.scrollTo(0, document.documentElement.scrollHeight);
+ window.scrollBy(0, -200);
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/inheritance.html b/testing/web-platform/tests/css/css-scroll-anchoring/inheritance.html
new file mode 100644
index 0000000000..035d4ffd2e
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/inheritance.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Inheritance of CSS Scroll Anchoring properties</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#property-index">
+<meta name="assert" content="overflow-anchor does not inherit.">
+<meta name="assert" content="overflow-anchor has initial value 'none'.">
+<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('overflow-anchor', 'auto', 'none');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/inline-block-002.html b/testing/web-platform/tests/css/css-scroll-anchoring/inline-block-002.html
new file mode 100644
index 0000000000..9163d34b3b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/inline-block-002.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>CSS Scroll Anchoring: Anchor node can be an empty inline-block</title>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#anchor-node-selection">
+<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/4247">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1540203">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+body { height: 4000px }
+#ib1, #ib2 { display: inline-block; height: 100px; width: 100% }
+</style>
+<span id=ib1></span>
+<br><br>
+<span id=ib2></span>
+<script>
+
+// Tests anchoring to an empty inline-block.
+
+test(() => {
+ document.scrollingElement.scrollTop = 150;
+ document.querySelector("#ib1").style.height = "150px";
+ assert_equals(document.scrollingElement.scrollTop, 200);
+}, "Anchor selection can select empty inline-blocks");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/inline-block.html b/testing/web-platform/tests/css/css-scroll-anchoring/inline-block.html
new file mode 100644
index 0000000000..881ff97de9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/inline-block.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px }
+#outer { line-height: 100px }
+#ib1, #ib2 { display: inline-block }
+
+</style>
+<span id=outer>
+ <span id=ib1>abc</span>
+ <br><br>
+ <span id=ib2>def</span>
+</span>
+<script>
+
+// Tests anchoring to an inline block inside a <span>.
+
+test(() => {
+ document.scrollingElement.scrollTop = 150;
+ document.querySelector("#ib1").style.lineHeight = "150px";
+ assert_equals(document.scrollingElement.scrollTop, 200);
+}, "Anchor selection descent into inline blocks.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/multicol-fragmented-anchor.html b/testing/web-platform/tests/css/css-scroll-anchoring/multicol-fragmented-anchor.html
new file mode 100644
index 0000000000..931a88ccbf
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/multicol-fragmented-anchor.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { margin: 0; }
+#scroller {
+ overflow: scroll;
+ height: 100px;
+}
+#multicol {
+ margin-top: 20px;
+ height: 200px;
+ columns: 2;
+ column-fill: auto;
+}
+#before {
+ margin-top: 100px;
+ height: 100px;
+}
+#content {
+ height: 10px;
+}
+</style>
+<div id="scroller">
+ <div id="multicol">
+ <div id="fragmented">
+ <div id="before"></div>
+ <div id="content">content</div>
+ </div>
+ </div>
+</div>
+<script>
+
+// Tests a scroll anchor inside of a div fragmented across multicol
+
+test(() => {
+ let scroller = document.querySelector("#scroller");
+ let before = document.querySelector("#before");
+ let content = document.querySelector("#content");
+
+ // Scroll down so that we select a scroll anchor. We should select #content
+ // and not #before, as #before is positioned offscreen in the first column
+ scroller.scrollTop = 10;
+
+ // Increase the height of #before so that it fragments into the second
+ // column and pushes #content down.
+ before.style.height = "110px";
+
+ // We should have anchored to #content and have done an adjustment of 10px
+ assert_equals(scroller.scrollTop, 20);
+}, "An element in a fragmented div should be able to be selected as an anchor node.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/negative-layout-overflow.html b/testing/web-platform/tests/css/css-scroll-anchoring/negative-layout-overflow.html
new file mode 100644
index 0000000000..e1ce331f1a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/negative-layout-overflow.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body {
+ height: 1200px;
+}
+#header {
+ position: relative;
+ height: 100px;
+}
+#evil {
+ position: relative;
+ top: -900px;
+ height: 1000px;
+ width: 100px;
+}
+#changer {
+ height: 100px;
+}
+#anchor {
+ height: 100px;
+ background-color: green;
+}
+
+</style>
+<div id="header">
+ <div id="evil"></div>
+</div>
+<div id="changer"></div>
+<div id="anchor"></div>
+<script>
+
+// Tests that the anchor selection algorithm correctly accounts for negative
+// positioning when computing bounds for visibility.
+
+test(() => {
+ document.scrollingElement.scrollTop = 250;
+ document.querySelector("#changer").style.height = "200px";
+ assert_equals(document.scrollingElement.scrollTop, 350);
+}, "Anchor selection accounts for negative positioning.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-ref.html b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-ref.html
new file mode 100644
index 0000000000..003cb9b68a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-ref.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <link rel="author" title="Chris Harrelson" href="mailto:chrishtr@chromium.org">
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+ <script src="/common/reftest-wait.js"></script>
+</head>
+<style>
+#outer {
+ overflow: hidden;
+ width: 500px;
+ height: 500px;
+}
+#inner {
+ overflow: auto;
+ position: relative;
+ width: 500px;
+ height: 2000px;
+}
+p {
+
+ font: 48pt monospace;
+}
+</style>
+</head>
+<body>
+<div id="outer">
+ <div id="inner">
+ <p>Anchor</p>
+ </div>
+</div>
+<script>
+const outer = document.querySelector("#outer");
+const inner = document.querySelector("#inner");
+
+onload = () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ outer.scrollTo(0, 70);
+ takeScreenshot();
+ });
+ });
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical-ref.html b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical-ref.html
new file mode 100644
index 0000000000..0026f2f888
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical-ref.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <link rel="author" title="Chris Harrelson" href="mailto:chrishtr@chromium.org">
+ <link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+ <script src="/common/reftest-wait.js"></script>
+</head>
+<style>
+#outer {
+ overflow: hidden;
+ width: 500px;
+ height: 500px;
+ writing-mode: vertical-rl;
+}
+#inner {
+ overflow: auto;
+ position: relative;
+ width: 2000px;
+ height: 500px;
+}
+p {
+ font: 48pt monospace;
+}
+</style>
+</head>
+<body>
+<div id="outer">
+ <div id="inner">
+ <p>Anchor</p>
+ </div>
+</div>
+<script>
+const outer = document.querySelector("#outer");
+const inner = document.querySelector("#inner");
+
+onload = () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ outer.scrollTo(-70, 0);
+ takeScreenshot();
+ });
+ });
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical.html b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical.html
new file mode 100644
index 0000000000..5b176a2042
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Test that subtree layout with nested overflow preserves scroll anchoring in vertical mode.</title>
+ <link rel="author" title="Chris Harrelson" href="mailto:chrishtr@chromium.org">
+ <link rel="author" title="Andreu Botella" href="mailto:abotella@igalia.com">
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+ <link rel="match" href="nested-overflow-subtree-layout-vertical-ref.html">
+ <script src="/common/reftest-wait.js"></script>
+</head>
+<style>
+#outer {
+ overflow: hidden;
+ width: 500px;
+ height: 500px;
+ writing-mode: vertical-rl;
+}
+#inner {
+ overflow: auto;
+ position: relative;
+ width: 2000px;
+ height: 500px;
+}
+p {
+ font: 48pt monospace;
+}
+</style>
+</head>
+<body>
+<div id="outer">
+ <div id="inner">
+ <p>Anchor</p>
+ </div>
+</div>
+<script>
+const outer = document.querySelector("#outer");
+const inner = document.querySelector("#inner");
+
+onload = () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ outer.scrollTo(-70, 0);
+ requestAnimationFrame(() => {
+ const elem = document.createElement("p");
+ elem.textContent = "FAIL";
+ inner.insertBefore(elem, inner.firstChild);
+ takeScreenshot();
+ });
+ });
+ });
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout.html b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout.html
new file mode 100644
index 0000000000..e7696016bb
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Test that subtree layout with nested overflow preserves scroll anchoring.</title>
+ <link rel="author" title="Chris Harrelson" href="mailto:chrishtr@chromium.org">
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+ <link rel="match" href="nested-overflow-subtree-layout-ref.html">
+ <script src="/common/reftest-wait.js"></script>
+</head>
+<style>
+#outer {
+ overflow: hidden;
+ width: 500px;
+ height: 500px;
+}
+#inner {
+ overflow: auto;
+ position: relative;
+ width: 500px;
+ height: 2000px;
+}
+p {
+
+ font: 48pt monospace;
+}
+</style>
+</head>
+<body>
+<div id="outer">
+ <div id="inner">
+ <p>Anchor</p>
+ </div>
+</div>
+<script>
+const outer = document.querySelector("#outer");
+const inner = document.querySelector("#inner");
+
+onload = () => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ outer.scrollTo(0, 70);
+ requestAnimationFrame(() => {
+ const elem = document.createElement("p");
+ elem.textContent = "FAIL";
+ inner.insertBefore(elem, inner.firstChild);
+ takeScreenshot();
+ });
+ });
+ });
+};
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic-scroller.html b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic-scroller.html
new file mode 100644
index 0000000000..6ccbc4f2fd
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic-scroller.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+ overflow: scroll;
+ width: 300px;
+ height: 300px;
+}
+#before { height: 50px; }
+#content { margin-top: 100px; margin-bottom: 600px; }
+.no { overflow-anchor: none; }
+
+</style>
+<div id="scroller">
+ <div id="before"></div>
+ <div id="content">content</div>
+</div>
+<script>
+
+// Tests that dynamic styling 'overflow-anchor' on a scrolling element has the
+// same effect as initial styling
+
+test(() => {
+ let scroller = document.querySelector("#scroller");
+ let before = document.querySelector("#before");
+
+ // Scroll down so that #content is the first element in the viewport
+ scroller.scrollTop = 100;
+
+ // Change the height of #before to trigger a scroll adjustment. This ensures
+ // that #content was selected as a scroll anchor
+ before.style.height = "100px";
+ assert_equals(scroller.scrollTop, 150);
+
+ // Now set 'overflow-anchor: none' on #scroller. This should invalidate the
+ // scroll anchor, and #scroller shouldn't be able to select an anchor anymore
+ scroller.className = 'no';
+
+ // Change the height of #before and make sure we don't adjust. This ensures
+ // that #content is not a scroll anchor
+ before.style.height = "150px";
+ assert_equals(scroller.scrollTop, 150);
+}, "Dynamically styling 'overflow-anchor: none' on the scroller element should prevent scroll anchoring");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic.html b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic.html
new file mode 100644
index 0000000000..ec548dc3d6
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+ overflow: scroll;
+ width: 300px;
+ height: 300px;
+}
+#before { height: 50px; }
+#content { margin-top: 100px; margin-bottom: 600px; }
+.no { overflow-anchor: none; }
+
+</style>
+<div id="scroller">
+ <div id="before"></div>
+ <div id="content">content</div>
+</div>
+<script>
+
+// Tests that dynamic styling 'overflow-anchor' on an anchor node has the
+// same effect as initial styling
+
+test(() => {
+ let scroller = document.querySelector("#scroller");
+ let before = document.querySelector("#before");
+ let content = document.querySelector("#content");
+
+ // Scroll down so that #content is the first element in the viewport
+ scroller.scrollTop = 100;
+
+ // Change the height of #before to trigger a scroll adjustment. This ensures
+ // that #content was selected as a scroll anchor
+ before.style.height = "100px";
+ assert_equals(scroller.scrollTop, 150);
+
+ // Now set 'overflow-anchor: none' on #content. This should invalidate the
+ // scroll anchor, and #scroller should recalculate its anchor. There are no
+ // other valid anchors in the viewport, so there should be no anchor.
+ content.className = 'no';
+
+ // Change the height of #before and make sure we don't adjust. This ensures
+ // that #content was not selected as a scroll anchor
+ before.style.height = "150px";
+ assert_equals(scroller.scrollTop, 150);
+}, "Dynamically styling 'overflow-anchor: none' on the anchor node should prevent scroll anchoring");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-inner-table.html b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-inner-table.html
new file mode 100644
index 0000000000..d5ec073821
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-inner-table.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+ height: 200px;
+ overflow: scroll;
+}
+#before { height: 50px; }
+#table-row {
+ display: table-row;
+ overflow-anchor: none;
+ width: 100px;
+ height: 100px;
+}
+#after { margin-bottom: 500px; }
+
+</style>
+<div id="scroller">
+ <div id="before"></div>
+ <div id="table-row">content</div>
+ <div id="after"></div>
+</div>
+<script>
+
+// Tests that the anchor exclusion API works with table parts that generate
+// anonymous table box wrappers
+
+test(() => {
+ let scroller = document.querySelector('#scroller');
+ let before = document.querySelector('#before');
+
+ // Scroll down so that #table-row is the only element in view
+ scroller.scrollTop = 50;
+
+ // Expand #before so that we might perform a scroll adjustment
+ before.style.height = "100px";
+
+ // We shouldn't have selected #table-row as an anchor as it is
+ // 'overflow-anchor: none'
+ assert_equals(scroller.scrollTop, 50);
+}, "A table with anonymous wrappers and 'overflow-anchor: none' shouldn't generate any scroll anchor candidates.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-table.html b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-table.html
new file mode 100644
index 0000000000..83cfef9797
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out-table.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#scroller {
+ height: 200px;
+ overflow: scroll;
+}
+#before { height: 50px; }
+#table {
+ display: table;
+ overflow-anchor: none;
+ width: 100px;
+ height: 100px;
+ margin-bottom: 500px;
+}
+
+</style>
+<div id="scroller">
+ <div id="before"></div>
+ <div id="table">content</div>
+</div>
+<script>
+
+// Tests that the anchor exclusion API works with tables
+
+test(() => {
+ let scroller = document.querySelector('#scroller');
+ let before = document.querySelector('#before');
+
+ // Scroll down so that #table is the only element in view
+ scroller.scrollTop = 50;
+
+ // Expand #before so that we might perform a scroll adjustment
+ before.style.height = "100px";
+
+ // We shouldn't have selected #table as an anchor as it is
+ // 'overflow-anchor: none'
+ assert_equals(scroller.scrollTop, 50);
+}, "A table with 'overflow-anchor: none' shouldn't generate any scroll anchor candidates.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/opt-out.html b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out.html
new file mode 100644
index 0000000000..12d46c13f9
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/opt-out.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 2000px; overflow-anchor: none; }
+#scroller { overflow: scroll; width: 500px; height: 300px; }
+.anchor {
+ position:relative; height: 100px; width: 150px;
+ background-color: #afa; border: 1px solid gray;
+}
+#forceScrolling { height: 500px; background-color: #fcc; }
+
+</style>
+<div id="outerChanger"></div>
+<div id="outerAnchor" class="anchor"></div>
+<div id="scroller">
+ <div id="innerChanger"></div>
+ <div id="innerAnchor" class="anchor"></div>
+ <div id="forceScrolling"></div>
+</div>
+<script>
+
+// Tests that scroll anchoring can be disabled per-scroller with the
+// overflow-anchor property.
+
+var divScroller = document.querySelector("#scroller");
+var docScroller = document.scrollingElement;
+var innerChanger = document.querySelector("#innerChanger");
+var outerChanger = document.querySelector("#outerChanger");
+
+function setup() {
+ divScroller.scrollTop = 100;
+ docScroller.scrollTop = 100;
+ innerChanger.style.height = "200px";
+ outerChanger.style.height = "150px";
+}
+
+function reset() {
+ document.body.style.overflowAnchor = "";
+ divScroller.style.overflowAnchor = "";
+ divScroller.scrollTop = 0;
+ docScroller.scrollTop = 0;
+ innerChanger.style.height = "";
+ outerChanger.style.height = "";
+}
+
+test(() => {
+ setup();
+
+ assert_equals(divScroller.scrollTop, 300,
+ "Scroll anchoring should apply to #scroller.");
+
+ assert_equals(docScroller.scrollTop, 100,
+ "Scroll anchoring should not apply to the document scroller.");
+
+ reset();
+}, "Disabled on document, enabled on div.");
+
+test(() => {
+ document.body.style.overflowAnchor = "auto";
+ divScroller.style.overflowAnchor = "none";
+ setup();
+
+ assert_equals(divScroller.scrollTop, 100,
+ "Scroll anchoring should not apply to #scroller.");
+
+ assert_equals(docScroller.scrollTop, 250,
+ "Scroll anchoring should apply to the document scroller.");
+
+ reset();
+}, "Enabled on document, disabled on div.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-computed.html b/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-computed.html
new file mode 100644
index 0000000000..16b41edb86
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-computed.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Anchoring: getComputedStyle().overflowAnchor</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#propdef-overflow-anchor">
+<meta name="assert" content="overflow-anchor 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("overflow-anchor", "auto");
+test_computed_value("overflow-anchor", "none");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-invalid.html b/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-invalid.html
new file mode 100644
index 0000000000..af66cd6e8a
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-invalid.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Anchoring: parsing overflow-anchor with invalid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#propdef-overflow-anchor">
+<meta name="assert" content="overflow-anchor supports only the grammar 'auto | none'.">
+<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("overflow-anchor", "all");
+test_invalid_value("overflow-anchor", "auto none");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-valid.html b/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-valid.html
new file mode 100644
index 0000000000..62b9761552
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-valid.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>CSS Scroll Anchoring: parsing overflow-anchor with valid values</title>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#propdef-overflow-anchor">
+<meta name="assert" content="overflow-anchor supports the full grammar 'auto | none'.">
+<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("overflow-anchor", "auto");
+test_valid_value("overflow-anchor", "none");
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-change.html b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-change.html
new file mode 100644
index 0000000000..6e00238382
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-change.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1543599">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers">
+<style>
+#hidden {
+ display: none;
+ background: red;
+}
+#spacer {
+ height: calc(100vh + 200px); /* At least 200px of scroll range */
+}
+</style>
+<table>
+ <thead>
+ <tr>
+ <th>1
+ <th>1
+ <th>1
+ <th>1
+ </tr>
+ </thead>
+ <thead id="hidden">
+ <tr>
+ <th>1
+ <th>1
+ <th>1
+ <th>1
+ </tr>
+ </thead>
+ <tbody>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ </tbody>
+</table>
+<div id="spacer"></div>
+<script>
+let firstEvent = true;
+const targetScrollPosition = 100;
+const hidden = document.querySelector("#hidden");
+const t = async_test("Scroll offset doesn't change when an element goes back and forth to display: none");
+window.onscroll = t.step_func(function() {
+ hidden.style.display = "block";
+ hidden.style.position = "absolute";
+ hidden.style.visibility = "hidden";
+ window.unused = hidden.offsetHeight;
+ hidden.style.display = "";
+ hidden.style.position = "";
+ hidden.style.visibility = "";
+
+ if (firstEvent) {
+ firstEvent = false;
+ requestAnimationFrame(t.step_func(function() {
+ requestAnimationFrame(t.step_func_done(function() {
+ assert_equals(document.scrollingElement.scrollTop, targetScrollPosition);
+ }));
+ }));
+ }
+});
+
+window.onload = t.step_func(function() {
+ window.scrollTo(0, targetScrollPosition);
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-to-abspos-change.html b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-to-abspos-change.html
new file mode 100644
index 0000000000..5cf27d9e6b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-to-abspos-change.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1568778">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers">
+<style>
+#hidden {
+ display: none;
+ background: red;
+}
+#spacer {
+ height: calc(100vh + 200px); /* At least 200px of scroll range */
+}
+</style>
+<table>
+ <thead>
+ <tr>
+ <th>1
+ <th>1
+ <th>1
+ <th>1
+ </tr>
+ </thead>
+ <thead id="hidden">
+ <tr>
+ <th>1
+ <th>1
+ <th>1
+ <th>1
+ </tr>
+ </thead>
+ <tbody>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ <tr><td>0 <td>0 <td>0 <td>0 </tr>
+ </tbody>
+</table>
+<div id="spacer"></div>
+<script>
+let isFirstEvent = true;
+const targetScrollPosition = 100;
+const hidden = document.querySelector("#hidden");
+const t = async_test("Scroll offset doesn't get stuck in infinite scroll events when an element goes back and forth to display: none while changing abspos style");
+window.onscroll = t.step_func(function() {
+ hidden.style.display = "block";
+ hidden.style.position = "absolute";
+ hidden.style.visibility = "hidden";
+ window.unused = hidden.offsetHeight;
+ hidden.style.display = "";
+ hidden.style.position = "";
+ hidden.style.visibility = "";
+
+ assert_true(isFirstEvent, "Shouldn't get more than one scroll event");
+ isFirstEvent = false;
+ requestAnimationFrame(t.step_func(function() {
+ requestAnimationFrame(t.step_func_done(function() {
+ assert_equals(document.scrollingElement.scrollTop, targetScrollPosition);
+ }));
+ }));
+});
+
+window.onload = t.step_func(function() {
+ window.scrollTo(0, targetScrollPosition);
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-ib-split.html b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-ib-split.html
new file mode 100644
index 0000000000..e903325112
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-ib-split.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io">
+<link rel="author" title="Mozilla" href="https://mozilla.org">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1559627">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers">
+<style>
+ body, h1 {
+ margin: 0
+ }
+ .sticky {
+ width: 100%;
+ top: 0;
+ left: 0;
+ }
+ .sticky > div {
+ width: 100%;
+ height: 50px;
+ background: darkblue;
+ }
+ header {
+ background: lightblue;
+ }
+ main {
+ background: lightgrey;
+ height: 200vh;
+ }
+</style>
+<header>
+ <h1>Some title</h1>
+ <span class="sticky"><div>Sticky header</div></span>
+</header>
+<main>
+ Some actual content.
+</main>
+<script>
+const sticky = document.querySelector(".sticky");
+const nonStickyOffset = sticky.firstElementChild.offsetTop;
+const t = async_test("Scroll offset adjustments are correctly suppressed when changing the position of an inline");
+let firstEvent = true;
+window.onscroll = t.step_func(function() {
+ sticky.style.position =
+ document.documentElement.scrollTop > nonStickyOffset ? "fixed" : "static";
+ if (firstEvent) {
+ firstEvent = false;
+ requestAnimationFrame(t.step_func(function() {
+ requestAnimationFrame(t.step_func_done(function() {
+ assert_equals(sticky.style.position, "fixed", "Element should become and remain fixed")
+ }));
+ }));
+ }
+});
+window.onload = t.step_func(function() {
+ window.scrollTo(0, nonStickyOffset + 1);
+});
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-in-nested-scroll-box.html b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-in-nested-scroll-box.html
new file mode 100644
index 0000000000..58c88001d5
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-in-nested-scroll-box.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/#suppression-triggers">
+<style>
+#space {
+ height: 4000px;
+ overflow: hidden;
+}
+#header {
+ background-color: #F5B335;
+ height: 50px;
+ width: 100%;
+}
+#content {
+ background-color: #D3D3D3;
+ height: 400px;
+}
+.scroller {
+ overflow: scroll;
+ position: relative;
+ width: 600px;
+ height: 600px;
+}
+body {
+ overflow: hidden;
+}
+</style>
+<div id="maybeScroller">
+ <div id="space">
+ <div id="header"></div>
+ <div id="before"></div>
+ <div id="content"></div>
+ </div>
+</div>
+<script>
+
+// Tests that scroll anchoring is suppressed when an element in the scroller
+// changes its in-flow state.
+
+var scroller;
+
+function runCase(oldPos, newPos, expectSuppression, skipInverse) {
+ var header = document.querySelector("#header");
+ var before = document.querySelector("#before");
+
+ header.style.position = oldPos;
+ before.style.height = "0";
+ scroller.scrollTop = 200;
+
+ header.style.position = newPos;
+ before.style.height = "25px";
+
+ var expectedTop = expectSuppression ? 200 : 225;
+ assert_equals(scroller.scrollTop, expectedTop);
+
+ if (!skipInverse)
+ runCase(newPos, oldPos, expectSuppression, true);
+}
+
+test(() => {
+ scroller = document.scrollingElement;
+ document.querySelector("#maybeScroller").className = "";
+
+ runCase("static", "fixed", true);
+ runCase("static", "absolute", true);
+ runCase("static", "relative", false);
+ runCase("fixed", "absolute", false);
+ runCase("fixed", "relative", true);
+ runCase("absolute", "relative", true);
+}, "Position changes in document scroller.");
+
+test(() => {
+ scroller = document.querySelector("#maybeScroller");
+ scroller.className = "scroller";
+
+ runCase("static", "fixed", true);
+ runCase("static", "absolute", true);
+ runCase("static", "relative", false);
+ runCase("fixed", "absolute", false);
+ runCase("fixed", "relative", true);
+ runCase("absolute", "relative", true);
+}, "Position changes in scrollable <div>.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic.html b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic.html
new file mode 100644
index 0000000000..b36b211f58
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+#space {
+ height: 4000px;
+}
+#header {
+ background-color: #F5B335;
+ height: 50px;
+ width: 100%;
+}
+#content {
+ background-color: #D3D3D3;
+ height: 400px;
+}
+.scroller {
+ overflow: scroll;
+ position: relative;
+ width: 600px;
+ height: 600px;
+}
+
+</style>
+<div id="maybeScroller">
+ <div id="space">
+ <div id="header"></div>
+ <div id="before"></div>
+ <div id="content"></div>
+ </div>
+</div>
+<script>
+
+// Tests that scroll anchoring is suppressed when an element in the scroller
+// changes its in-flow state.
+
+var scroller;
+
+function runCase(oldPos, newPos, expectSuppression, skipInverse) {
+ var header = document.querySelector("#header");
+ var before = document.querySelector("#before");
+
+ header.style.position = oldPos;
+ before.style.height = "0";
+ scroller.scrollTop = 200;
+
+ header.style.position = newPos;
+ before.style.height = "25px";
+
+ var expectedTop = expectSuppression ? 200 : 225;
+ assert_equals(scroller.scrollTop, expectedTop);
+
+ if (!skipInverse)
+ runCase(newPos, oldPos, expectSuppression, true);
+}
+
+test(() => {
+ scroller = document.scrollingElement;
+ document.querySelector("#maybeScroller").className = "";
+
+ runCase("static", "fixed", true);
+ runCase("static", "absolute", true);
+ runCase("static", "relative", false);
+ runCase("fixed", "absolute", false);
+ runCase("fixed", "relative", true);
+ runCase("absolute", "relative", true);
+}, "Position changes in document scroller.");
+
+test(() => {
+ scroller = document.querySelector("#maybeScroller");
+ scroller.className = "scroller";
+
+ runCase("static", "fixed", true);
+ runCase("static", "absolute", true);
+ runCase("static", "relative", false);
+ runCase("fixed", "absolute", false);
+ runCase("fixed", "relative", true);
+ runCase("absolute", "relative", true);
+}, "Position changes in scrollable <div>.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/reading-scroll-forces-anchoring.html b/testing/web-platform/tests/css/css-scroll-anchoring/reading-scroll-forces-anchoring.html
new file mode 100644
index 0000000000..39b8e36398
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/reading-scroll-forces-anchoring.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+body { height: 1000px }
+div { height: 100px }
+</style>
+<div id="block1">abc</div>
+<div id="block2">def</div>
+<script>
+ // This test verifies that reading window.scrollY forces any pending scroll
+ // anchoring adjustment to occur before computing the return value.
+ async_test((t) => {
+ scrollTo(0, 150);
+ requestAnimationFrame(() => {
+ step_timeout(() => {
+ // Queue scroll anchoring adjustment.
+ document.querySelector("#block1").style.height = "200px";
+
+ // Reading scrollY should force both the layout and the adjustment to
+ // occur synchronously.
+ var y = scrollY;
+
+ assert_equals(y, 250);
+ t.done();
+ }, 0);
+ });
+ }, 'Reading scroll position forces scroll anchoring adjustment.');
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/scroll-padding-affects-anchoring.html b/testing/web-platform/tests/css/css-scroll-anchoring/scroll-padding-affects-anchoring.html
new file mode 100644
index 0000000000..e2c8741d88
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/scroll-padding-affects-anchoring.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<title>scroll anchoring accounts for scroll-padding</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://drafts.csswg.org/css-scroll-anchoring/#anchor-node-selection">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-snap-1/#optimal-viewing-region">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ #scroller {
+ overflow: auto;
+ height: 500px;
+ scroll-padding-top: 200px;
+ }
+ #changer {
+ height: 100px;
+ }
+ #content {
+ height: 1000px;
+ }
+</style>
+<div id="scroller">
+ <div id="changer"></div>
+ <div id="content"></div>
+</div>
+<script>
+ test(() => {
+ scroller.scrollTop = 50;
+ changer.style.height = "200px";
+ assert_equals(scroller.scrollTop, 150, "Shouldn't anchor to #changer, since it's covered by scroll-padding");
+ });
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html b/testing/web-platform/tests/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html
new file mode 100644
index 0000000000..043844d056
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<meta name="viewport" content="user-scalable=no"/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { margin: 0; }
+html {
+ line-height: 0;
+ width: 200vw;
+ height: 200vh;
+}
+
+html.ltr { direction: ltr; }
+html.rtl { direction: rtl; }
+
+html.horz { writing-mode: horizontal-tb; }
+html.vlr { writing-mode: vertical-lr; }
+html.vrl { writing-mode: vertical-rl; }
+
+.horz.ltr .cx2, .vlr .cx2 { left: 100vw; }
+.horz.rtl .cx2, .vrl .cx2 { right: 100vw; }
+.horz .cy2, .ltr .cy2 { top: 100vh; }
+.vlr.rtl .cy2, .vrl.rtl .cy2 { bottom: 100vh; }
+
+#block_pusher, #inline_pusher {
+ display: inline-block;
+ width: 100px;
+ height: 100px;
+}
+#block_pusher { background-color: #e88; }
+#inline_pusher { background-color: #88e; }
+.vpush { height: 80px !important; }
+.hpush { width: 70px !important; }
+
+#anchor-container {
+ display: inline-block;
+}
+#anchor {
+ position: relative;
+ background-color: #8e8;
+ min-width: 100px;
+ min-height: 100px;
+}
+
+#grower { width: 0; height: 0; }
+.grow {
+ width: 180px !important;
+ height: 160px !important;
+}
+
+</style>
+<div id="container">
+ <div id="block_pusher"></div><br>
+ <div id="inline_pusher"></div><div id="anchor-container">
+ <div id="anchor">
+ <div id="grower"></div>
+ </div>
+ </div>
+</div>
+<script>
+
+// Tests that anchoring adjustments are only on the block layout axis and that
+// their magnitude is based on the movement of the block start edge of the
+// anchor node, for all 6 combinations of text direction and writing mode,
+// regardless of which corner of the viewport the anchor node overlaps.
+
+var CORNERS = ["cx1 cy1", "cx2 cy1", "cx1 cy2", "cx2 cy2"];
+var docEl = document.documentElement;
+var scroller = document.scrollingElement;
+var blockPusher = document.querySelector("#block_pusher");
+var inlinePusher = document.querySelector("#inline_pusher");
+var grower = document.querySelector("#grower");
+var anchor = document.querySelector("#anchor");
+
+function reset() {
+ scroller.scrollLeft = 0;
+ scroller.scrollTop = 0;
+ blockPusher.className = "";
+ inlinePusher.className = "";
+ grower.className = "";
+}
+
+function runCase(docClass, xDir, yDir, vert, expectXAdj, expectYAdj, corner) {
+ docEl.className = docClass;
+ anchor.className = corner;
+
+ var initX = 150 * xDir;
+ var initY = 150 * yDir;
+
+ scroller.scrollLeft = initX;
+ scroller.scrollTop = initY;
+
+ // Each corner moves a different distance.
+ block_pusher.className = vert ? "hpush" : "vpush";
+ inline_pusher.className = vert ? "vpush" : "hpush";
+ grower.className = "grow";
+
+ assert_equals(scroller.scrollLeft, initX + expectXAdj);
+ assert_equals(scroller.scrollTop, initY + expectYAdj);
+
+ reset();
+}
+
+test(() => {
+ CORNERS.forEach((corner) => {
+ runCase("horz ltr", 1, 1, false, 0, -20, corner);
+ });
+}, "Horizontal LTR.");
+
+test(() => {
+ CORNERS.forEach((corner) => {
+ runCase("horz rtl", -1, 1, false, 0, -20, corner);
+ });
+}, "Horizontal RTL.");
+
+test(() => {
+ CORNERS.forEach((corner) => {
+ runCase("vlr ltr", 1, 1, true, -30, 0, corner);
+ });
+}, "Vertical-LR LTR.");
+
+test(() => {
+ CORNERS.forEach((corner) => {
+ runCase("vlr rtl", 1, -1, true, -30, 0, corner);
+ });
+}, "Vertical-LR RTL.");
+
+test(() => {
+ CORNERS.forEach((corner) => {
+ runCase("vrl ltr", -1, 1, true, 30, 0, corner);
+ });
+}, "Vertical-RL LTR.");
+
+test(() => {
+ CORNERS.forEach((corner) => {
+ runCase("vrl rtl", -1, -1, true, 30, 0, corner);
+ });
+}, "Vertical-RL RTL.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/subtree-exclusion.html b/testing/web-platform/tests/css/css-scroll-anchoring/subtree-exclusion.html
new file mode 100644
index 0000000000..25961b3664
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/subtree-exclusion.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body { height: 4000px }
+#A, #B { width: 100px; background-color: #afa; }
+#B { height: 100px; }
+#inner { width: 100px; height: 100px; background-color: pink; }
+#A { overflow-anchor: none; }
+
+</style>
+<div id="changer1"></div>
+<div id="A">
+ <div id="inner"></div>
+ <div id="changer2"></div>
+</div>
+<div id="B"></div>
+<script>
+
+// Tests that an element with overflow-anchor: none is excluded, along with its
+// DOM descendants, from the anchor selection algorithm.
+
+test(() => {
+ var changer1 = document.querySelector("#changer1");
+ var changer2 = document.querySelector("#changer2");
+
+ document.scrollingElement.scrollTop = 50;
+ changer1.style.height = "100px";
+ changer2.style.height = "50px";
+
+ // We should anchor to #B, not #A or #inner, despite them being visible.
+ assert_equals(document.scrollingElement.scrollTop, 200);
+
+ document.querySelector("#A").style.overflowAnchor = "auto";
+ document.scrollingElement.scrollTop = 150;
+
+ changer1.style.height = "200px";
+ changer2.style.height = "100px";
+
+ // We should now anchor to #inner, which is moved only by #changer1.
+ assert_equals(document.scrollingElement.scrollTop, 250);
+}, "Subtree exclusion with overflow-anchor.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/support/flexbox-scrolling-vertical-rl.html b/testing/web-platform/tests/css/css-scroll-anchoring/support/flexbox-scrolling-vertical-rl.html
new file mode 100644
index 0000000000..1a2d02d5c7
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/support/flexbox-scrolling-vertical-rl.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<style>
+ html {
+ writing-mode: vertical-rl;
+ overflow: hidden;
+ background: red;
+ }
+ body {
+ margin: 0;
+ }
+</style>
+<div style="display:flex; flex-direction:column-reverse;">
+ <div style="block-size:1000px;"></div>
+ <div style="block-size:100px; background:green;"></div>
+ <div style="block-size:100px;"></div>
+</div>
+<script>
+ window.scrollTo(-100, 0);
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/support/history-restore-anchors-new-window.html b/testing/web-platform/tests/css/css-scroll-anchoring/support/history-restore-anchors-new-window.html
new file mode 100644
index 0000000000..bd8290e793
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/support/history-restore-anchors-new-window.html
@@ -0,0 +1,29 @@
+<style>
+ body {
+ margin: 0px;
+ height: 2000px;
+ width: 2000px;
+ }
+
+ #first {
+ height: 1000px;
+ background-color: #FFA5D2;
+ }
+
+ #anchor {
+ position: absolute;
+ background-color: #84BE6A;
+ height: 600px;
+ width: 100%;
+ }
+</style>
+
+<div id="first"></div>
+<div id="changer"></div>
+<div id="anchor"></div>
+
+<script>
+onload = function() {
+ opener.postMessage("loaded", "*");
+}
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/support/scrolling-vertical-rl.html b/testing/web-platform/tests/css/css-scroll-anchoring/support/scrolling-vertical-rl.html
new file mode 100644
index 0000000000..1273469dab
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/support/scrolling-vertical-rl.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<style>
+ html {
+ writing-mode: vertical-rl;
+ overflow: hidden;
+ background: red;
+ }
+ body {
+ margin: 0;
+ }
+</style>
+<div style="block-size:100px;"></div>
+<div style="block-size:100px; background:green;"></div>
+<div style="block-size:1000px;"></div>
+<script>
+ window.scrollTo(-100, 0);
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/table-collapsed-borders-crash.html b/testing/web-platform/tests/css/css-scroll-anchoring/table-collapsed-borders-crash.html
new file mode 100644
index 0000000000..aa699317e2
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/table-collapsed-borders-crash.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html class="test-wait">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=746570">
+<meta name="assert" content="No crash when a table with dirty internal layout is the scroll anchor."/>
+<style>
+body {
+ height:200vh;
+}
+table {
+ height: 200px;
+ width: 200px;
+ background-color: lime;
+ border-collapse: collapse; /* triggers problematic border calculation */
+}
+</style>
+
+<table id=table1></table>
+
+<script>
+ window.scrollBy(0, 10);
+ table1.innerHTML = "<tr><td style='background-color:lightblue'></td></tr>";
+ document.documentElement.classList.remove('test-wait');
+</script>
+</html>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/text-anchor-in-vertical-rl.html b/testing/web-platform/tests/css/css-scroll-anchoring/text-anchor-in-vertical-rl.html
new file mode 100644
index 0000000000..0edf950936
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/text-anchor-in-vertical-rl.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mstensho@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+
+<p>There should be no red below.</p>
+<div id="container" style="writing-mode:vertical-rl; overflow:auto; width:300px; height:300px;">
+ <div style="width:300px; background:red;"></div>
+ <div style="width:400px; font-size:16px; line-height:25px;">
+ <span id="displayMe" style="color:red; display:none;">
+ FAIL<br>FAIL<br>FAIL<br>FAIL<br>
+ </span>
+ line<br>
+ </div>
+ <div id="displayMeToo" style="display:none; width:300px; background:red;"></div>
+</div>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+ test(()=> {
+ var container = document.getElementById("container");
+ var displayMe = document.getElementById("displayMe");
+ var displayMeToo = document.getElementById("displayMeToo");
+ // Scroll the text container into view.
+ container.scrollLeft = -300;
+ displayMe.style.display = "inline";
+ displayMeToo.style.display = "block";
+ assert_equals(container.scrollLeft, -400);
+ }, "Line at edge of scrollport shouldn't jump visually when content is inserted before");
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-000.html b/testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-000.html
new file mode 100644
index 0000000000..ee367b1c97
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-000.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1124195">
+<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
+<style>
+ #iframe {
+ display: block;
+ border: none;
+ width: 300px;
+ height: 100px;
+ }
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<iframe id="iframe" src="support/scrolling-vertical-rl.html"></iframe>
+<script>
+ onload = function() {
+ document.body.offsetTop;
+ iframe.style.width = "100px";
+ }
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-001.html b/testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-001.html
new file mode 100644
index 0000000000..0e58a1e63b
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-001.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org">
+<link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1124195">
+<link rel="match" href="../reference/ref-filled-green-100px-square.xht">
+<style>
+ #iframe {
+ display: block;
+ border: none;
+ width: 300px;
+ height: 100px;
+ }
+</style>
+<p>Test passes if there is a filled green square and <strong>no red</strong>.</p>
+<iframe id="iframe" src="support/flexbox-scrolling-vertical-rl.html"></iframe>
+<script>
+ onload = function() {
+ document.body.offsetTop;
+ iframe.style.width = "100px";
+ }
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/wrapped-text.html b/testing/web-platform/tests/css/css-scroll-anchoring/wrapped-text.html
new file mode 100644
index 0000000000..60f11fbcfe
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/wrapped-text.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+
+body {
+ position: absolute;
+ font-size: 100px;
+ width: 200px;
+ height: 4000px;
+ line-height: 100px;
+}
+
+</style>
+abc <b id=b>def</b> ghi
+<script>
+
+// Tests anchoring to a text node that is moved by preceding text.
+
+test(() => {
+ var b = document.querySelector("#b");
+ var preText = b.previousSibling;
+ document.scrollingElement.scrollTop = 150;
+ preText.nodeValue = "abcd efg ";
+ assert_equals(document.scrollingElement.scrollTop, 250);
+}, "Anchoring with text wrapping changes.");
+
+</script>
diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/zero-scroll-offset.html b/testing/web-platform/tests/css/css-scroll-anchoring/zero-scroll-offset.html
new file mode 100644
index 0000000000..b8f5aa2ccc
--- /dev/null
+++ b/testing/web-platform/tests/css/css-scroll-anchoring/zero-scroll-offset.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<head>
+ <title>Test that scroll anchoring is suppressed when scroll offset is zero.</title>
+ <link rel="author" title="Nick Burris" href="mailto:nburris@chromium.org">
+ <link rel="help" href="https://drafts.csswg.org/css-scroll-anchoring/">
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+</head>
+<style>
+#header {
+ height: 100px;
+ border: 1px solid black;
+ overflow-anchor: none;
+}
+#content {
+ height: 200vh;
+}
+</style>
+<div id="header"></div>
+<div id="content">abc</div>
+<script>
+window.addEventListener("scroll", function() {
+ if (document.scrollingElement.scrollTop > 0) {
+ // On the first scroll event, shrink the header. Scroll anchoring anchors to
+ // content, but the header shrinks by more than the scroll offset so the
+ // resulting scroll position is zero.
+ step_timeout(function() {
+ document.querySelector("#header").style.height = "50px";
+ }, 0);
+ } else {
+ // On the second scroll event, grow the header. Since the scroll offset is
+ // zero, scroll anchoring should be suppressed. Otherwise, scroll anchoring
+ // would anchor to content and the resulting scroll position would be 50px.
+ step_timeout(function() {
+ document.querySelector("#header").style.height = "100px";
+ }, 0);
+ }
+});
+
+async_test(function(t) {
+ // Scroll down a bit to trigger the scroll event listener.
+ window.scrollTo(0, 10);
+
+ window.requestAnimationFrame(function() {
+ window.requestAnimationFrame(function() {
+ window.requestAnimationFrame(t.step_func_done(() => {
+ assert_equals(document.scrollingElement.scrollTop, 0);
+ }));
+ });
+ });
+
+}, "Scroll anchoring suppressed when scroll offset is zero.");
+</script>