From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel.baumann@progress-linux.org>
Date: Sun, 7 Apr 2024 21:33:14 +0200
Subject: Adding upstream version 115.7.0esr.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
---
 .../tests/css/css-scroll-anchoring/META.yml        |   3 +
 .../tests/css/css-scroll-anchoring/README.md       |   8 ++
 .../abspos-containing-block-outside-scroller.html  |  55 ++++++++
 ...abspos-contributes-to-static-parent-bounds.html |  40 ++++++
 .../abspos-in-multicol-001.html                    |  38 ++++++
 .../abspos-in-multicol-002.html                    |  43 +++++++
 .../abspos-in-multicol-003.html                    |  39 ++++++
 ...ustments-in-scroll-event-handler.tentative.html |  53 ++++++++
 .../ancestor-change-heuristic.html                 |  81 ++++++++++++
 .../css-scroll-anchoring/anchor-inside-iframe.html |  27 ++++
 .../anchor-updates-after-explicit-scroll.html      |  51 ++++++++
 .../anchoring-with-bounds-clamping-div.html        |  38 ++++++
 .../anchoring-with-bounds-clamping.html            |  28 ++++
 .../css-scroll-anchoring/anonymous-block-box.html  |  34 +++++
 .../tests/css/css-scroll-anchoring/basic.html      |  23 ++++
 .../clamp-negative-overflow.html                   |  61 +++++++++
 .../clipped-scrollers-skipped.html                 |  38 ++++++
 .../contain-paint-offscreen-container.html         |  42 ++++++
 .../descend-into-container-with-float.html         |  36 ++++++
 .../descend-into-container-with-overflow.html      |  30 +++++
 .../device-pixel-adjustment.html                   |  77 +++++++++++
 .../dirty-contents-reselect-anchor.tentative.html  |  54 ++++++++
 .../exclude-fixed-position.html                    |  26 ++++
 .../css/css-scroll-anchoring/exclude-inline.html   |  34 +++++
 .../css/css-scroll-anchoring/exclude-sticky.html   |  28 ++++
 .../css-scroll-anchoring/focus-prioritized.html    |  46 +++++++
 .../fragment-scrolling-anchors.html                |  60 +++++++++
 .../css/css-scroll-anchoring/fullscreen-crash.html |  33 +++++
 ...h-offset-update-from-scroll-event-listener.html |  59 +++++++++
 .../heuristic-with-offset-update.html              |  58 +++++++++
 .../history-restore-anchors.html                   |  36 ++++++
 .../tests/css/css-scroll-anchoring/image-001.html  |  29 +++++
 .../infinite-scroll-event.tentative.html           |  42 ++++++
 .../css/css-scroll-anchoring/inheritance.html      |  21 +++
 .../css/css-scroll-anchoring/inline-block-002.html |  28 ++++
 .../css/css-scroll-anchoring/inline-block.html     |  26 ++++
 .../multicol-fragmented-anchor.html                |  56 ++++++++
 .../negative-layout-overflow.html                  |  44 +++++++
 .../nested-overflow-subtree-layout-ref.html        |  44 +++++++
 ...ested-overflow-subtree-layout-vertical-ref.html |  45 +++++++
 .../nested-overflow-subtree-layout-vertical.html   |  52 ++++++++
 .../nested-overflow-subtree-layout.html            |  51 ++++++++
 .../opt-out-dynamic-scroller.html                  |  49 +++++++
 .../css/css-scroll-anchoring/opt-out-dynamic.html  |  51 ++++++++
 .../css-scroll-anchoring/opt-out-inner-table.html  |  47 +++++++
 .../css/css-scroll-anchoring/opt-out-table.html    |  45 +++++++
 .../tests/css/css-scroll-anchoring/opt-out.html    |  74 +++++++++++
 .../parsing/overflow-anchor-computed.html          |  19 +++
 .../parsing/overflow-anchor-invalid.html           |  18 +++
 .../parsing/overflow-anchor-valid.html             |  18 +++
 ...ition-change-heuristic-display-none-change.html |  72 +++++++++++
 ...ge-heuristic-display-none-to-abspos-change.html |  71 +++++++++++
 .../position-change-heuristic-ib-split.html        |  57 +++++++++
 ...tion-change-heuristic-in-nested-scroll-box.html |  85 +++++++++++++
 .../position-change-heuristic.html                 |  82 ++++++++++++
 .../reading-scroll-forces-anchoring.html           |  30 +++++
 .../scroll-padding-affects-anchoring.html          |  32 +++++
 .../start-edge-in-block-layout-direction.html      | 141 +++++++++++++++++++++
 .../css-scroll-anchoring/subtree-exclusion.html    |  45 +++++++
 .../support/flexbox-scrolling-vertical-rl.html     |  20 +++
 .../history-restore-anchors-new-window.html        |  29 +++++
 .../support/scrolling-vertical-rl.html             |  18 +++
 .../table-collapsed-borders-crash.html             |  25 ++++
 .../text-anchor-in-vertical-rl.html                |  30 +++++
 .../vertical-rl-viewport-size-change-000.html      |  21 +++
 .../vertical-rl-viewport-size-change-001.html      |  21 +++
 .../css/css-scroll-anchoring/wrapped-text.html     |  28 ++++
 .../css-scroll-anchoring/zero-scroll-offset.html   |  53 ++++++++
 68 files changed, 2898 insertions(+)
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/META.yml
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/README.md
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/abspos-containing-block-outside-scroller.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/abspos-contributes-to-static-parent-bounds.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-001.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-002.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/abspos-in-multicol-003.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/adjustments-in-scroll-event-handler.tentative.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/ancestor-change-heuristic.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/anchor-inside-iframe.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/anchor-updates-after-explicit-scroll.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping-div.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/anchoring-with-bounds-clamping.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/anonymous-block-box.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/basic.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/clamp-negative-overflow.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/clipped-scrollers-skipped.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/contain-paint-offscreen-container.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-float.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/descend-into-container-with-overflow.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/device-pixel-adjustment.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/dirty-contents-reselect-anchor.tentative.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/exclude-fixed-position.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/exclude-inline.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/exclude-sticky.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/focus-prioritized.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/fragment-scrolling-anchors.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/fullscreen-crash.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update-from-scroll-event-listener.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/heuristic-with-offset-update.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/history-restore-anchors.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/image-001.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/infinite-scroll-event.tentative.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/inheritance.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/inline-block-002.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/inline-block.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/multicol-fragmented-anchor.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/negative-layout-overflow.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-ref.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical-ref.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout-vertical.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/nested-overflow-subtree-layout.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic-scroller.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/opt-out-dynamic.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/opt-out-inner-table.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/opt-out-table.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/opt-out.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-computed.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-invalid.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/parsing/overflow-anchor-valid.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-change.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-display-none-to-abspos-change.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-ib-split.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic-in-nested-scroll-box.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/position-change-heuristic.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/reading-scroll-forces-anchoring.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/scroll-padding-affects-anchoring.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/start-edge-in-block-layout-direction.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/subtree-exclusion.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/support/flexbox-scrolling-vertical-rl.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/support/history-restore-anchors-new-window.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/support/scrolling-vertical-rl.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/table-collapsed-borders-crash.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/text-anchor-in-vertical-rl.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-000.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/vertical-rl-viewport-size-change-001.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/wrapped-text.html
 create mode 100644 testing/web-platform/tests/css/css-scroll-anchoring/zero-scroll-offset.html

(limited to 'testing/web-platform/tests/css/css-scroll-anchoring')

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/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/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/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>
-- 
cgit v1.2.3