Import Cobalt 21.master.0.253153
diff --git a/src/third_party/web_platform_tests/intersection-observer/META.yml b/src/third_party/web_platform_tests/intersection-observer/META.yml
new file mode 100644
index 0000000..31dddab
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/IntersectionObserver/
+suggested_reviewers:
+  - szager-chromium
diff --git a/src/third_party/web_platform_tests/intersection-observer/bounding-box.html b/src/third_party/web_platform_tests/intersection-observer/bounding-box.html
new file mode 100644
index 0000000..367243d
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/bounding-box.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  overflow: visible;
+  height: 200px;
+  width: 160px;
+  border: 8px solid black;
+}
+#target {
+  margin: 10px;
+  width: 100px;
+  height: 100px;
+  padding: 10px;
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target" style="transform: translateY(300px)"></div>
+</div>
+
+<script>
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Test that the target's border bounding box is used to calculate intersection.");
+
+function step0() {
+  var targetBounds = clientBounds(target);
+  target.style.transform = "translateY(195px)";
+  runTestCycle(step1, "target.style.transform = 'translateY(195px)'");
+  checkLastEntry(entries, 0, targetBounds.concat(0, 0, 0, 0, 8, 184, 8, 224, false));
+}
+
+function step1() {
+  var targetBounds = clientBounds(target);
+  target.style.transform = "translateY(300px)";
+  runTestCycle(step2, "target.style.transform = 'translateY(300px)'");
+  checkLastEntry(entries, 1, targetBounds.concat(26, 146, 221, 224, 8, 184, 8, 224, true));
+}
+
+function step2() {
+  var targetBounds = clientBounds(target);
+  target.style.transform = "";
+  target.style.zoom = "2";
+  runTestCycle(step3, "target.style.zoom = 2");
+  checkLastEntry(entries, 2, targetBounds.concat(0, 0, 0, 0, 8, 184, 8, 224, false));
+}
+
+function step3() {
+  var targetBounds = clientBounds(target);
+  var intersectionWidth = (
+      176  // root width including border
+      -8   // root left border
+      -20  // target left margin * target zoom
+  ) / 2;   // convert to target's zoom factor.
+  var intersectionHeight = (216 - 8 - 20) / 2;
+  var intersectionRect = [targetBounds[0], targetBounds[0] + intersectionWidth,
+                          targetBounds[2], targetBounds[2] + intersectionHeight];
+  checkLastEntry(entries, 3, targetBounds.concat(intersectionRect).concat(8, 184, 8, 224, true));
+}
+
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/client-rect.html b/src/third_party/web_platform_tests/intersection-observer/client-rect.html
new file mode 100644
index 0000000..e85171c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/client-rect.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 180px;
+  height: 100px;
+}
+</style>
+
+<iframe id="iframe" srcdoc="<div id='target' style='margin:0.5px;width:1000px;height:1000px;'></div>"></iframe>
+
+<script>
+var target;
+var entries = [];
+var observer;
+var iframe = document.getElementById("iframe");
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes);
+    });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(test0, "First rAF should generate notification.");
+  }, "IntersectionObserverEntry.boundingClientRect should match target.boundingClientRect()");
+};
+
+function test0() {
+  assert_equals(entries.length, 1, "One notification.");
+  var bcr = target.getBoundingClientRect();
+  checkLastEntry(entries, 0, [bcr.left, bcr.right, bcr.top, bcr.bottom]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/containing-block.html b/src/third_party/web_platform_tests/intersection-observer/containing-block.html
new file mode 100644
index 0000000..f7ce6fa
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/containing-block.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  width: 170px;
+  height: 200px;
+  overflow-y: scroll;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+  position: absolute;
+}
+</style>
+
+<div id="root" style="position: absolute">
+  <div id="target" style="left: 50px; top: 250px"></div>
+</div>
+
+<script>
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  root = document.getElementById("root");
+  assert_true(!!root, "root element exists.");
+  target = document.getElementById("target");
+  assert_true(!!target, "target element exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  }, { root: root });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  target.style.top = "10px";
+  runTestCycle(test1, "In containing block and intersecting.");
+}, "IntersectionObserver should only report intersections if root is a containing block ancestor of target.");
+
+function test1() {
+  runTestCycle(test2, "In containing block and not intersecting.");
+  var rootBounds = contentBounds(root);
+  checkLastEntry(entries, 0, [58, 158, 18, 118, 58, 158, 18, 118].concat(rootBounds));
+  target.style.top = "250px";
+}
+
+function test2() {
+  runTestCycle(test3, "Not in containing block and intersecting.");
+  var rootBounds = contentBounds(root);
+  checkLastEntry(entries, 1, [58, 158, 258, 358, 0, 0, 0, 0].concat(rootBounds));
+  root.style.position = "static";
+  target.style.top = "10px";
+}
+
+function test3() {
+  runTestCycle(test4, "Not in containing block and not intersecting.");
+  checkLastEntry(entries, 1);
+  target.style.top = "250px";
+}
+
+function test4() {
+  checkLastEntry(entries, 1);
+  target.style.top = "0";
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/cross-origin-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/cross-origin-iframe.sub.html
new file mode 100644
index 0000000..d444237
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/cross-origin-iframe.sub.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 160px;
+  height: 100px;
+  overflow-y: scroll;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+
+<div class="spacer"></div>
+<iframe src="http://{{hosts[alt][]}}:{{ports[http][0]}}/intersection-observer/resources/cross-origin-subframe.html" sandbox="allow-scripts"></iframe>
+<div class="spacer"></div>
+
+<script>
+async_test(function(t) {
+  var iframe = document.querySelector("iframe");
+
+  function handleMessage(event) {
+    if (event.data.hasOwnProperty('scrollTo')) {
+      document.scrollingElement.scrollTop = event.data.scrollTo;
+      waitForNotification(t, function() { iframe.contentWindow.postMessage("", "*"); },
+        "document.scrollingElement.scrollTop = " + event.data.scrollTo);
+    } else if (event.data.hasOwnProperty('actual')) {
+      checkJsonEntries(event.data.actual, event.data.expected, event.data.description);
+    } else if (event.data.hasOwnProperty('DONE')) {
+      document.scrollingElement.scrollTop = 0;
+      t.done();
+    } else {
+      var description = event.data.description;
+      waitForNotification(t, function() { iframe.contentWindow.postMessage("", "*"); }, description);
+    }
+  }
+
+  window.addEventListener("message", t.step_func(handleMessage));
+
+  iframe.onload = t.step_func(function() {
+    waitForNotification(t, function() { iframe.contentWindow.postMessage("", "*") }, "setup");
+  });
+}, "Intersection observer test with no explicit root and target in a cross-origin iframe.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/disconnect.html b/src/third_party/web_platform_tests/intersection-observer/disconnect.html
new file mode 100644
index 0000000..9c02daf
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/disconnect.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var entries = [];
+var observer;
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver should not deliver pending notifications after disconnect().");
+
+function step0() {
+  runTestCycle(step1, "observer.disconnect()");
+  document.scrollingElement.scrollTop = 300;
+  observer.disconnect();
+  assert_equals(entries.length, 1, "Initial notification.");
+}
+
+function step1() {
+  assert_equals(entries.length, 1, "No new notifications.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/display-none.html b/src/third_party/web_platform_tests/intersection-observer/display-none.html
new file mode 100644
index 0000000..cae3509
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/display-none.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  background-color: green;
+  width: 100px;
+  height: 100px;
+}
+</style>
+
+<div id="target"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById("target");
+  var root = document.getElementById("root");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "Intersecting notification after first rAF.");
+}, "IntersectionObserver should send a not-intersecting notification for a target that gets display:none.");
+
+function step0() {
+  runTestCycle(step1, "Not-intersecting notification after setting display:none on target.");
+  checkLastEntry(entries, 0, [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true]);
+  target.style.display = "none";
+}
+
+function step1() {
+  runTestCycle(step2, "Intersecting notification after removing display:none on target.");
+  checkLastEntry(entries, 1, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false]);
+  target.style.display = "";
+}
+
+function step2() {
+  checkLastEntry(entries, 2, [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/document-scrolling-element-root.html b/src/third_party/web_platform_tests/intersection-observer/document-scrolling-element-root.html
new file mode 100644
index 0000000..443ff2e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/document-scrolling-element-root.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  height: 250px;
+  width: 150px;
+  border: 0;
+}
+</style>
+<iframe id="target-iframe" src="resources/iframe-no-root-subframe.html"></iframe>
+
+<script>
+var iframe = document.getElementById("target-iframe");
+var target;
+var root;
+var entries = [];
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    assert_true(!!iframe, "iframe exists");
+
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    var observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    }, { root: iframe.contentDocument });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(step0, "First rAF.");
+  }, "Observer with explicit root which is the document.");
+};
+
+function step0() {
+  let vw = iframe.contentDocument.documentElement.clientWidth;
+  let vh = iframe.contentDocument.documentElement.clientHeight;
+  // The target element is partially clipped by the iframe's root scroller, so
+  // height of the intersection rect is (250 - 208) == 42.
+  checkLastEntry(entries, 0, [8, 108, 208, 308, 8, 108, 208, 250, 0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/edge-inclusive-intersection.html b/src/third_party/web_platform_tests/intersection-observer/edge-inclusive-intersection.html
new file mode 100644
index 0000000..b73c407
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/edge-inclusive-intersection.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  width: 200px;
+  height: 200px;
+  overflow: visible;
+}
+#target {
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target" style="width: 100px; height: 100px; transform: translateY(250px)"></div>
+</div>
+
+<script>
+var entries = [];
+
+runTestCycle(function() {
+  var root = document.getElementById('root');
+  assert_true(!!root, "root element exists.");
+  var target = document.getElementById('target');
+  assert_true(!!target, "target element exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  }, { root: root });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver should detect and report edge-adjacent and zero-area intersections.");
+
+function step0() {
+  runTestCycle(step1, "Set transform=translateY(200px) on target.");
+  checkLastEntry(entries, 0, [8, 108, 258, 358, 0, 0, 0, 0, 8, 208, 8, 208, false]);
+  target.style.transform = "translateY(200px)";
+}
+
+function step1() {
+  runTestCycle(step2, "Set transform=translateY(201px) on target.");
+  checkLastEntry(entries, 1, [8, 108, 208, 308, 8, 108, 208, 208, 8, 208, 8, 208, true]);
+  target.style.transform = "translateY(201px)";
+}
+
+function step2() {
+  runTestCycle(step3, "Set transform=translateY(185px) on target.");
+  checkLastEntry(entries, 2);
+  target.style.height = "0px";
+  target.style.width = "300px";
+  target.style.transform = "translateY(185px)";
+}
+
+function step3() {
+  checkLastEntry(entries, 3, [8, 308, 193, 193, 8, 208, 193, 193, 8, 208, 8, 208, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/empty-root-margin.html b/src/third_party/web_platform_tests/intersection-observer/empty-root-margin.html
new file mode 100644
index 0000000..9eaf856
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/empty-root-margin.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div id="target"></div>
+
+<script>
+var target = document.getElementById("target");
+async_test((t) => {
+    var observer = new IntersectionObserver(t.step_func_done((entries) => {
+        var rootBounds = entries[0].rootBounds;
+        assert_equals(rootBounds.left, 0);
+        assert_equals(rootBounds.right, document.documentElement.clientWidth);
+        assert_equals(rootBounds.top, 0);
+        assert_equals(rootBounds.bottom, document.documentElement.clientHeight);
+        observer.disconnect();
+    }), { rootMargin: "" });
+    observer.observe(document.getElementById("target"));
+}, "An empty rootMargin string is interpreted as a margin of size zero");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/idlharness.window.js b/src/third_party/web_platform_tests/intersection-observer/idlharness.window.js
new file mode 100644
index 0000000..2059e1c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/idlharness.window.js
@@ -0,0 +1,22 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+// https://w3c.github.io/IntersectionObserver/
+
+idl_test(
+  ['intersection-observer'],
+  ['dom'],
+  idl_array => {
+    idl_array.add_objects({
+      IntersectionObserver: ['observer'],
+    });
+    var options = {
+      root: document.body,
+      rootMargin: '0px',
+      threshold: 1.0
+    }
+    self.observer = new IntersectionObserver(() => {}, options);
+  }
+);
diff --git a/src/third_party/web_platform_tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html
new file mode 100644
index 0000000..28e6d09
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+iframe {
+  height: 100px;
+  width: 150px;
+}
+</style>
+
+<div class="spacer"></div>
+<div style="overflow: hidden">
+  <iframe id="target-iframe" src="resources/iframe-no-root-subframe.html"></iframe>
+</div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var iframe = document.getElementById("target-iframe");
+var target;
+var entries = [];
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    assert_true(!!iframe, "iframe exists");
+
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    var observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(step0, "First rAF.");
+  }, "Observer with the implicit root; target in a same-origin iframe.");
+};
+
+function step0() {
+  document.scrollingElement.scrollTop = 200;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 200");
+  checkLastEntry(entries, 0, [8, 108, 208, 308, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  iframe.contentDocument.scrollingElement.scrollTop = 250;
+  runTestCycle(step2, "iframe.contentDocument.scrollingElement.scrollTop = 250");
+  assert_equals(entries.length, 1, "entries.length == 1");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 108, -42, 58, 8, 108, 0, 58, 0, vw, 0, vh, true]);
+}
+
+function step3() {
+  checkLastEntry(entries, 2, [8, 108, -42, 58, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+  document.scrollingElement.scrollTop = 0;
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/iframe-no-root.html b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root.html
new file mode 100644
index 0000000..8532246
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/iframe-no-root.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+iframe {
+  height: 100px;
+  width: 150px;
+}
+</style>
+
+<div class="spacer"></div>
+<iframe id="target-iframe" src="resources/iframe-no-root-subframe.html"></iframe>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var iframe = document.getElementById("target-iframe");
+var target;
+var entries = [];
+
+iframe.onload = function() {
+  runTestCycle(function() {
+    assert_true(!!iframe, "iframe exists");
+
+    target = iframe.contentDocument.getElementById("target");
+    assert_true(!!target, "Target element exists.");
+    var observer = new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    });
+    observer.observe(target);
+    entries = entries.concat(observer.takeRecords());
+    assert_equals(entries.length, 0, "No initial notifications.");
+    runTestCycle(step0, "First rAF.");
+  }, "Observer with the implicit root; target in a same-origin iframe.");
+};
+
+function step0() {
+  document.scrollingElement.scrollTop = 200;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 200");
+  checkLastEntry(entries, 0, [8, 108, 208, 308, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  iframe.contentDocument.scrollingElement.scrollTop = 250;
+  runTestCycle(step2, "iframe.contentDocument.scrollingElement.scrollTop = 250");
+  assert_equals(entries.length, 1, "entries.length == 1");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 108, -42, 58, 8, 108, 0, 58, 0, vw, 0, vh, true]);
+}
+
+function step3() {
+  checkLastEntry(entries, 2, [8, 108, -42, 58, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+  document.scrollingElement.scrollTop = 0;
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/initial-observation-with-threshold.html b/src/third_party/web_platform_tests/intersection-observer/initial-observation-with-threshold.html
new file mode 100644
index 0000000..b9218b0
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/initial-observation-with-threshold.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 240px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  margin: 200px 0 0 0;
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target"></div>
+</div>
+
+<script>
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { root: root, threshold: [0.5] });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "First observation with a threshold.");
+
+function step0() {
+  root.scrollTop = 20;
+  runTestCycle(step1, "root.scrollTop = 20");
+  checkLastEntry(entries, 0, [ 11, 111, 211, 311, 11, 111, 211, 251, 11, 111, 11, 251, false]);
+}
+
+function step1() {
+  checkLastEntry(entries, 1, [ 11, 111, 191, 291, 11, 111, 191, 251, 11, 111, 11, 251, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/inline-client-rect.html b/src/third_party/web_platform_tests/intersection-observer/inline-client-rect.html
new file mode 100644
index 0000000..c096230
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/inline-client-rect.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 120px;
+  left: 0;
+}
+#scroller {
+  width: 250px;
+  overflow: auto;
+}
+#overflow {
+  width: 1000px;
+}
+.content {
+  width: 100px;
+  height: 20px;
+  padding: 40px 0;
+  text-align: center;
+  background-color: grey;
+  display: inline-block;
+}
+</style>
+
+<div id="scroller">
+  <div id="overflow">
+    <span><div class="content">1</div></span>
+    <span><div class="content">2</div></span>
+    <span><div class="content">3</div></span>
+    <span id="target"><div class="content">4</div></span>
+    <span><div class="content">5</div></span>
+  </div>
+</div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var scroller, target, spaceWidth, targetOffsetLeft, targetOffsetTop;
+
+runTestCycle(function() {
+  scroller = document.getElementById("scroller");
+  assert_true(!!scroller, "scroller exists");
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Inline target");
+
+function step0() {
+  // Measure space width between two adjacent inlines.
+  let nextEl = target.nextElementSibling;
+  spaceWidth = nextEl.offsetLeft - target.offsetLeft - target.offsetWidth;
+  // 8px body margin + 3 preceding siblings @ (100px width + spaceWidth) each
+  targetOffsetLeft = 8 + 300 + (spaceWidth * 3);
+  // 8px body margin + 40px top padding
+  targetOffsetTop = 48;
+  let left = targetOffsetLeft;
+  let right = left + 100;
+  let top = targetOffsetTop;
+  let bottom = top + target.offsetHeight;
+
+  scroller.scrollLeft = 90;
+  runTestCycle(step1, "scroller.scrollLeft = 90");
+
+  checkLastEntry(entries, 0, [left, right, top, bottom,
+                              0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  // -90px for scroll offset
+  let left = targetOffsetLeft - 90;
+  let right = left + 100;
+  let top = targetOffsetTop;
+  let bottom = top + target.offsetHeight;
+  // 8px body margin + 250px client width of scroller
+  let scrollerRight = 258;
+  checkLastEntry(entries, 1, [left, right, top, bottom,
+                              left, scrollerRight, top, bottom,
+                              0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/inline-with-block-child-client-rect.html b/src/third_party/web_platform_tests/intersection-observer/inline-with-block-child-client-rect.html
new file mode 100644
index 0000000..81a8fd1
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/inline-with-block-child-client-rect.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 120px;
+  left: 0;
+}
+#target {
+  display: inline;
+}
+</style>
+
+<div id="target">
+  <div>
+    <img width=100 height=100 />
+  </div>
+</div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Inline target containing a block child");
+
+function step0() {
+  assert_equals(entries.length, 1);
+  checkRect(entries[0].boundingClientRect, clientBounds(target),
+            "entry.boundingClientRect == target.getBoundingClientRect()");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/isIntersecting-change-events.html b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-change-events.html
new file mode 100644
index 0000000..99bc65b
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-change-events.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 150px;
+  height: 200px;
+  overflow-y: scroll;
+}
+#target1, #target2, #target3, #target4 {
+  width: 100px;
+  height: 100px;
+}
+#target1 {
+  background-color: green;
+}
+#target2 {
+  background-color: red;
+}
+#target3 {
+  background-color: blue;
+}
+#target4 {
+  background-color: yellow;
+}
+</style>
+
+<div id="root">
+  <div id="target1"></div>
+  <div id="target2"></div>
+  <div id="target3"></div>
+</div>
+
+<script>
+var entries = [];
+var observer;
+
+runTestCycle(function() {
+  var root = document.getElementById('root');
+  var target1 = document.getElementById('target1');
+  var target2 = document.getElementById('target2');
+  var target3 = document.getElementById('target3');
+  assert_true(!!root, "root element exists.");
+  assert_true(!!target1, "target1 element exists.");
+  assert_true(!!target2, "target2 element exists.");
+  assert_true(!!target3, "target3 element exists.");
+  observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  }, { root: root });
+  observer.observe(target1);
+  observer.observe(target2);
+  observer.observe(target3);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "Rects in initial notifications should report initial positions.");
+}, "isIntersecting changes should trigger notifications.");
+
+function step0() {
+  assert_equals(entries.length, 3, "Has 3 initial notifications.");
+  checkRect(entries[0].boundingClientRect, [0, 100, 0, 100], "Check 1st entry rect");
+  assert_equals(entries[0].target.id, 'target1', "Check 1st entry target id.");
+  checkIsIntersecting(entries, 0, true);
+  checkRect(entries[1].boundingClientRect, [0, 100, 100, 200], "Check 2nd entry rect");
+  assert_equals(entries[1].target.id, 'target2', "Check 2nd entry target id.");
+  checkIsIntersecting(entries, 1, true);
+  checkRect(entries[2].boundingClientRect, [0, 100, 200, 300], "Check 3rd entry rect");
+  assert_equals(entries[2].target.id, 'target3', "Check 3rd entry target id.");
+  checkIsIntersecting(entries, 2, true);
+  runTestCycle(step1, "Set scrollTop=100 and check for no new notifications.");
+  root.scrollTop = 100;
+}
+
+function step1() {
+  assert_equals(entries.length, 3, "Has 3 total notifications because isIntersecting did not change.");
+  runTestCycle(step2, "Add 4th target.");
+  root.scrollTop = 0;
+  var target4 = document.createElement('div');
+  target4.setAttribute('id', 'target4');
+  root.appendChild(target4);
+  observer.observe(target4);
+}
+
+function step2() {
+  assert_equals(entries.length, 4, "Has 4 total notifications because 4th element was added.");
+  checkRect(entries[3].boundingClientRect, [0, 100, 300, 400], "Check 4th entry rect");
+  assert_equals(entries[3].target.id, 'target4', "Check 4th entry target id.");
+  checkIsIntersecting(entries, 3, false);
+  assert_equals(entries[3].intersectionRatio, 0, 'target4 initially has intersectionRatio of 0.');
+  runTestCycle(step3, "Set scrollTop=100 and check for one new notification.");
+  root.scrollTop = 100;
+}
+
+function step3() {
+  assert_equals(entries.length, 5, "Has 5 total notifications.");
+  checkRect(entries[4].boundingClientRect, [0, 100, 200, 300], "Check 5th entry rect");
+  assert_equals(entries[4].target.id, 'target4', "Check 5th entry target id.");
+  checkIsIntersecting(entries, 4, true);
+  assert_equals(entries[4].intersectionRatio, 0, 'target4 still has intersectionRatio of 0.');
+  root.scrollTop = 0; // reset to make it easier to refresh and run the test
+}
+
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/isIntersecting-threshold.html b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-threshold.html
new file mode 100644
index 0000000..106b65e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/isIntersecting-threshold.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+<style>
+#scroller { width: 100px; height: 100px; overflow: scroll; }
+#scroller > div { height: 800px; }
+#target { margin-top: 25px; height: 50px; background-color: blue; }
+</style>
+<div id="scroller">
+  <div>
+    <div id="target"></div>
+  </div>
+</div>
+
+<script>
+let entries = [];
+
+window.onload = function() {
+  runTestCycle(step2, "At initial scroll position");
+
+  scroller.scrollTop = 0;
+  let observer = new IntersectionObserver(
+    es => entries = entries.concat(es),
+    { threshold: 1 }
+  );
+  observer.observe(target);
+};
+
+function step2() {
+  runTestCycle(step3, "Scrolled to half way through target element");
+
+  assert_equals(entries.length, 1);
+  assert_equals(entries[0].intersectionRatio, 1);
+  assert_equals(entries[0].isIntersecting, true);
+  scroller.scrollTop = 50;
+}
+
+function step3() {
+  runTestCycle(step4, "Scrolled to target element completely off screen");
+
+  assert_equals(entries.length, 2);
+  assert_true(entries[1].intersectionRatio >= 0.5 &&
+              entries[1].intersectionRatio < 1);
+  assert_equals(entries[1].isIntersecting, true);
+  scroller.scrollTop = 100;
+}
+
+function step4() {
+  assert_equals(entries.length, 2);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html b/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html
new file mode 100644
index 0000000..22353e3
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/multiple-targets.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+.target {
+  width: 100px;
+  height: 100px;
+  margin: 10px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target1" class="target"></div>
+<div id="target2" class="target"></div>
+<div id="target3" class="target"></div>
+
+<script>
+var entries = [];
+var target1, target2, target3;
+
+runTestCycle(function() {
+  target1 = document.getElementById("target1");
+  assert_true(!!target1, "target1 exists.");
+  target2 = document.getElementById("target2");
+  assert_true(!!target2, "target2 exists.");
+  target3 = document.getElementById("target3");
+  assert_true(!!target3, "target3 exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target1);
+  observer.observe(target2);
+  observer.observe(target3);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "One observer with multiple targets.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 150;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 150");
+  assert_equals(entries.length, 3, "Three initial notifications.");
+  assert_equals(entries[0].target, target1, "entries[0].target === target1");
+  assert_equals(entries[1].target, target2, "entries[1].target === target2");
+  assert_equals(entries[2].target, target3, "entries[2].target === target3");
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 10000;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 10000");
+  assert_equals(entries.length, 4, "Four notifications.");
+  assert_equals(entries[3].target, target1, "entries[3].target === target1");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 0");
+  assert_equals(entries.length, 6, "Six notifications.");
+  assert_equals(entries[4].target, target2, "entries[4].target === target2");
+  assert_equals(entries[5].target, target3, "entries[5].target === target3");
+}
+
+function step3() {
+  assert_equals(entries.length, 9, "Nine notifications.");
+  assert_equals(entries[6].target, target1, "entries[6].target === target1");
+  assert_equals(entries[7].target, target2, "entries[7].target === target2");
+  assert_equals(entries[8].target, target3, "entries[8].target === target3");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html b/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html
new file mode 100644
index 0000000..3599e1f
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/multiple-thresholds.html
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { threshold: [0, 0.25, 0.5, 0.75, 1] });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Observer with multiple thresholds.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 120;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 120");
+  checkLastEntry(entries, 0, [8, 108, vh + 108, vh + 208, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 160;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 160");
+  checkLastEntry(entries, 1, [8, 108, vh - 12, vh + 88, 8, 108, vh - 12, vh, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 200;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 200");
+  checkLastEntry(entries, 2, [8, 108, vh - 52, vh + 48, 8, 108, vh - 52, vh, 0, vw, 0, vh, true]);
+}
+
+function step3() {
+  document.scrollingElement.scrollTop = 240;
+  runTestCycle(step4, "document.scrollingElement.scrollTop = 240");
+  checkLastEntry(entries, 3, [8, 108, vh - 92, vh + 8, 8, 108, vh - 92, vh, 0, vw, 0, vh, true]);
+}
+
+function step4() {
+  document.scrollingElement.scrollTop = vh + 140;
+  runTestCycle(step5, "document.scrollingElement.scrollTop = window.innerHeight + 140");
+  checkLastEntry(entries, 4, [8, 108, vh - 132, vh - 32, 8, 108, vh - 132, vh - 32, 0, vw, 0, vh, true]);
+}
+
+function step5() {
+  document.scrollingElement.scrollTop = vh + 160;
+  runTestCycle(step6, "document.scrollingElement.scrollTop = window.innerHeight + 160");
+  checkLastEntry(entries, 5, [8, 108, -32, 68, 8, 108, 0, 68, 0, vw, 0, vh, true]);
+}
+
+function step6() {
+  document.scrollingElement.scrollTop = vh + 200;
+  runTestCycle(step7, "document.scrollingElement.scrollTop = window.innerHeight + 200");
+  checkLastEntry(entries, 6, [8, 108, -52, 48, 8, 108, 0, 48, 0, vw, 0, vh, true]);
+}
+
+function step7() {
+  checkLastEntry(entries, 7, [8, 108, -92, 8, 8, 108, 0, 8, 0, vw, 0, vh, true]);
+  document.scrollingElement.scrollTop = vh + 220;
+  runTestCycle(step8, "document.scrollingElement.scrollTop = window.innerHeight + 220");
+}
+
+function step8() {
+  checkLastEntry(entries, 8, [8, 108, -112, -12, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+  document.scrollingElement.scrollTop = 0;
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/nested-cross-origin-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/nested-cross-origin-iframe.sub.html
new file mode 100644
index 0000000..090d236
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/nested-cross-origin-iframe.sub.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/css/cssom-view/support/scroll-behavior.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+<style>
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+<div class="spacer"></div>
+<iframe id="iframe"></iframe>
+<script>
+
+promise_test(async t => {
+  iframe.src =      // non secure port.
+    get_host_info().HTTP_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html";
+
+  await new Promise(resolve => {
+    window.addEventListener("message", event => {
+      if (event.data == "ready") {
+        resolve();
+      }
+    }, { once: true });
+  });
+
+  let isIntersecting = false;
+  window.addEventListener("message", function listener(event) {
+    if (event.origin == get_host_info().HTTPS_NOTSAMESITE_ORIGIN) {
+      isIntersecting = event.data;
+      window.removeEventListener("message", listener);
+    }
+  });
+
+  await new Promise(resolve => waitForNotification(t, resolve));
+  await new Promise(resolve => waitForNotification(t, resolve));
+
+  assert_false(isIntersecting,
+               "The target element is not intersecting in all ancestor viewports");
+
+  // Scroll the iframe in this document into view, but still the target element
+  // in the grand child document is out of the child iframe's viewport.
+  iframe.scrollIntoView({ behavior: "instant" });
+
+  await waitForScrollEnd(document.scrollingElement);
+
+  assert_false(isIntersecting,
+               "The target element is not intersecting in all ancestor viewports");
+
+  // Now make the target element visible in the child iframe's viewport.
+  frames[0].postMessage("scroll", "*");
+
+  await new Promise(resolve => {
+    window.addEventListener("message", function listener(event) {
+      // It's possible that the message from the IntersectionObserver in the
+      // grand child document (HTTPS_NORSAMESITE_ORIGIN) is delivered ealier
+      // than scrollEnd message from the child document
+      // (HTTP_NOTSAMESITE_ORIGIN), so we need to differentiate them.
+      if (event.origin == get_host_info().HTTP_NOTSAMESITE_ORIGIN &&
+          event.data == "scrollEnd" ) {
+        window.removeEventListener("message", listener);
+        resolve();
+      }
+    });
+  });
+
+  await new Promise(resolve => waitForNotification(t, resolve));
+
+  assert_true(isIntersecting,
+              "The target element is now intersecting in all ancestor viewports");
+}, "IntersectionObserver with `implicit root` in a nested cross-origin iframe works");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-attributes.html b/src/third_party/web_platform_tests/intersection-observer/observer-attributes.html
new file mode 100644
index 0000000..4e2e529
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-attributes.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="root"></div>
+
+<script>
+test(function() {
+  var observer = new IntersectionObserver(function(e) {}, {});
+  test(function() { assert_equals(observer.root, null) },
+       "observer.root");
+  test(function() { assert_array_equals(observer.thresholds, [0]) },
+       "observer.thresholds");
+  test(function() { assert_equals(observer.rootMargin, "0px 0px 0px 0px") },
+       "observer.rootMargin");
+
+  var rootDiv = document.getElementById("root");
+  observer = new IntersectionObserver(function(e) {}, {
+    root: rootDiv,
+    threshold: [0, 0.25, 0.5, 1.0],
+    rootMargin: "10% 20px"
+  });
+  test(function() { assert_equals(observer.root, rootDiv) },
+       "set observer.root");
+  test(function() { assert_array_equals(observer.thresholds, [0, 0.25, 0.5, 1.0]) },
+       "set observer.thresholds");
+  test(function() { assert_equals(observer.rootMargin, "10% 20px 10% 20px") },
+       "set observer.rootMargin");
+}, "Observer attribute getters.");
+
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-exceptions.html b/src/third_party/web_platform_tests/intersection-observer/observer-exceptions.html
new file mode 100644
index 0000000..126790f
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-exceptions.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<script>
+test(function () {
+  assert_throws_js(RangeError, function() {
+    new IntersectionObserver(e => {}, {threshold: [1.1]})
+  })
+}, "IntersectionObserver constructor with { threshold: [1.1] }");
+
+test(function () {
+  assert_throws_js(TypeError, function() {
+    new IntersectionObserver(e => {}, {threshold: ["foo"]})
+  })
+}, 'IntersectionObserver constructor with { threshold: ["foo"] }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "1"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "1" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "2em"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "2em" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "auto"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "auto" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "calc(1px + 2px)"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "calc(1px + 2px)" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "1px !important"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "1px !important" }');
+
+test(function () {
+  assert_throws_dom("SYNTAX_ERR", function() {
+    new IntersectionObserver(e => {}, {rootMargin: "1px 1px 1px 1px 1px"})
+  })
+}, 'IntersectionObserver constructor with { rootMargin: "1px 1px 1px 1px 1px" }');
+
+test(function () {
+  assert_throws_js(TypeError, function() {
+    let observer = new IntersectionObserver(c => {}, {});
+    observer.observe("foo");
+  })
+}, 'IntersectionObserver.observe("foo")');
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-in-iframe.html b/src/third_party/web_platform_tests/intersection-observer/observer-in-iframe.html
new file mode 100644
index 0000000..e918bf1
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-in-iframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+</style>
+<iframe id="target-iframe" src="resources/observer-in-iframe-subframe.html" width="150px" height="150px"></iframe>
diff --git a/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html b/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html
new file mode 100644
index 0000000..53100c50
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/observer-without-js-reference.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  function createObserver() {
+    new IntersectionObserver(function(changes) {
+      entries = entries.concat(changes)
+    }).observe(target);
+  }
+  createObserver();
+  runTestCycle(step0, "First rAF");
+}, "IntersectionObserver that is unreachable in js should still generate notifications.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  assert_equals(entries.length, 1, "One notification.");
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 0;
+  assert_equals(entries.length, 2, "Two notifications.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/remove-element.html b/src/third_party/web_platform_tests/intersection-observer/remove-element.html
new file mode 100644
index 0000000..a093b22
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/remove-element.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 200px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.spacer {
+  height: 300px;
+}
+</style>
+
+<div id="root">
+  <div id="leading-space" class="spacer"></div>
+  <div id="target"></div>
+  <div id="trailing-space" class="spacer"</div>
+</div>
+
+<script>
+var entries = [];
+var root, target, trailingSpace;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  trailingSpace = document.getElementById("trailing-space");
+  assert_true(!!trailingSpace, "TrailingSpace exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "Root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Verify that not-intersecting notifications are sent when a target is removed from the DOM tree.");
+
+function step0() {
+  root.scrollTop = 150;
+  runTestCycle(step1, "root.scrollTop = 150");
+  checkLastEntry(entries, 0, [11, 111, 311, 411, 0, 0, 0, 0, 11, 111, 11, 211, false]);
+}
+
+function step1() {
+  root.removeChild(target);
+  runTestCycle(step2, "root.removeChild(target).");
+  checkLastEntry(entries, 1, [11, 111, 161, 261, 11, 111, 161, 211, 11, 111, 11, 211, true]);
+}
+
+function step2() {
+  root.scrollTop = 0;
+  root.insertBefore(target, trailingSpace);
+  runTestCycle(step3, "root.insertBefore(target, trailingSpace).");
+  checkLastEntry(entries, 2, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false]);
+}
+
+function step3() {
+  root.scrollTop = 150;
+  runTestCycle(step4, "root.scrollTop = 150 after reinserting target.");
+  checkLastEntry(entries, 2);
+}
+
+function step4() {
+  checkLastEntry(entries, 3, [11, 111, 161, 261, 11, 111, 161, 211, 11, 111, 11, 211, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-child-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-child-iframe.sub.html
new file mode 100644
index 0000000..8e2c36e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-child-iframe.sub.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+iframe.src =
+  get_host_info().ORIGIN + "/intersection-observer/resources/same-origin-grand-child-iframe.html";
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-subframe.html
new file mode 100644
index 0000000..4305ed1
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/cross-origin-subframe.html
@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<div style="height: 200px; width: 100px;"></div>
+<div id="target" style="background-color: green; width:100px; height:100px"></div>
+<div id="empty-target" style="width: 100px"></div>
+<div style="height: 200px; width: 100px;"></div>
+
+<script>
+var port;
+var entries = [];
+var target = document.getElementById("target");
+var emptyTarget = document.getElementById("empty-target");
+var scroller = document.scrollingElement;
+var nextStep;
+
+function clientRectToJson(rect) {
+  if (!rect)
+    return "null";
+  return {
+    top: rect.top,
+    right: rect.right,
+    bottom: rect.bottom,
+    left: rect.left
+  };
+}
+
+function entryToJson(entry) {
+  return {
+    boundingClientRect: clientRectToJson(entry.boundingClientRect),
+    intersectionRect: clientRectToJson(entry.intersectionRect),
+    rootBounds: clientRectToJson(entry.rootBounds),
+    isIntersecting: entry.isIntersecting,
+    target: entry.target.id
+  };
+}
+
+// Note that we never use RAF in this code, because this frame might get render-throttled.
+// Instead of RAF-ing, we just post an empty message to the parent window, which will
+// RAF when it is received, and then send us a message to cause the next step to run.
+
+// Use a rootMargin here, and verify it does NOT get applied for the cross-origin case.
+var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+}, { rootMargin: "7px" });
+observer.observe(target);
+observer.observe(emptyTarget);
+
+function step0() {
+  entries = entries.concat(observer.takeRecords());
+  nextStep = step1;
+  var expected = [{
+    boundingClientRect: [8, 108, 208, 308],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: target.id
+  }, {
+    boundingClientRect: [8, 108, 308, 308],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: emptyTarget.id
+  }];
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: expected,
+    description: "First rAF"
+  }, "*");
+  entries = [];
+  port.postMessage({scrollTo: 200}, "*");
+}
+
+function step1() {
+  entries = entries.concat(observer.takeRecords());
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: [],
+    description: "topDocument.scrollingElement.scrollTop = 200"
+  }, "*");
+  entries = [];
+  scroller.scrollTop = 250;
+  nextStep = step2;
+  port.postMessage({}, "*");
+}
+
+function step2() {
+  entries = entries.concat(observer.takeRecords());
+  var expected = [{
+    boundingClientRect: [8, 108, -42, 58],
+    intersectionRect: [8, 108, 0, 58],
+    rootBounds: "null",
+    isIntersecting: true,
+    target: target.id
+  }, {
+    boundingClientRect: [8, 108, 58, 58],
+    intersectionRect: [8, 108, 58, 58],
+    rootBounds: "null",
+    isIntersecting: true,
+    target: emptyTarget.id
+  }];
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: expected,
+    description: "iframeDocument.scrollingElement.scrollTop = 250"
+  }, "*");
+  entries = [];
+  nextStep = step3;
+  port.postMessage({scrollTo: 100}, "*");
+}
+
+function step3() {
+  entries = entries.concat(observer.takeRecords());
+  var expected = [{
+    boundingClientRect: [8, 108, -42, 58],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: target.id
+  }, {
+    boundingClientRect: [8, 108, 58, 58],
+    intersectionRect: [0, 0, 0, 0],
+    rootBounds: "null",
+    isIntersecting: false,
+    target: emptyTarget.id
+  }];
+  port.postMessage({
+    actual: entries.map(entryToJson),
+    expected: expected,
+    description: "topDocument.scrollingElement.scrollTop = 100"
+  }, "*");
+  port.postMessage({DONE: 1}, "*");
+}
+
+function handleMessage(event)
+{
+  port = event.source;
+  nextStep();
+}
+
+nextStep = step0;
+window.addEventListener("message", handleMessage);
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/iframe-no-root-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/iframe-no-root-subframe.html
new file mode 100644
index 0000000..ee63a06
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/iframe-no-root-subframe.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<div style="height: 200px; width: 100px;"></div>
+<div id="target" style="background-color: green; width:100px; height:100px"></div>
+<div style="height: 200px; width: 100px;"></div>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/intersection-observer-test-utils.js b/src/third_party/web_platform_tests/intersection-observer/resources/intersection-observer-test-utils.js
new file mode 100644
index 0000000..7db26d7
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/intersection-observer-test-utils.js
@@ -0,0 +1,196 @@
+// Here's how waitForNotification works:
+//
+// - myTestFunction0()
+//   - waitForNotification(myTestFunction1)
+//     - requestAnimationFrame()
+//   - Modify DOM in a way that should trigger an IntersectionObserver callback.
+// - BeginFrame
+//   - requestAnimationFrame handler runs
+//     - Second requestAnimationFrame()
+//   - Style, layout, paint
+//   - IntersectionObserver generates new notifications
+//     - Posts a task to deliver notifications
+// - Task to deliver IntersectionObserver notifications runs
+//   - IntersectionObserver callbacks run
+// - Second requestAnimationFrameHandler runs
+//     - step_timeout()
+// - step_timeout handler runs
+//   - myTestFunction1()
+//     - [optional] waitForNotification(myTestFunction2)
+//       - requestAnimationFrame()
+//     - Verify newly-arrived IntersectionObserver notifications
+//     - [optional] Modify DOM to trigger new notifications
+//
+// Ideally, it should be sufficient to use requestAnimationFrame followed
+// by two step_timeouts, with the first step_timeout firing in between the
+// requestAnimationFrame handler and the task to deliver notifications.
+// However, the precise timing of requestAnimationFrame, the generation of
+// a new display frame (when IntersectionObserver notifications are
+// generated), and the delivery of these events varies between engines, making
+// this tricky to test in a non-flaky way.
+//
+// In particular, in WebKit, requestAnimationFrame and the generation of
+// a display frame are two separate tasks, so a step_timeout called within
+// requestAnimationFrame can fire before a display frame is generated.
+//
+// In Gecko, on the other hand, requestAnimationFrame and the generation of
+// a display frame are a single task, and IntersectionObserver notifications
+// are generated during this task. However, the task posted to deliver these
+// notifications can fire after the following requestAnimationFrame.
+//
+// This means that in general, by the time the second requestAnimationFrame
+// handler runs, we know that IntersectionObservations have been generated,
+// and that a task to deliver these notifications has been posted (though
+// possibly not yet delivered). Then, by the time the step_timeout() handler
+// runs, these notifications have been delivered.
+//
+// Since waitForNotification uses a double-rAF, it is now possible that
+// IntersectionObservers may have generated more notifications than what is
+// under test, but have not yet scheduled the new batch of notifications for
+// delivery. As a result, observer.takeRecords should NOT be used in tests:
+//
+// - myTestFunction0()
+//   - waitForNotification(myTestFunction1)
+//     - requestAnimationFrame()
+//   - Modify DOM in a way that should trigger an IntersectionObserver callback.
+// - BeginFrame
+//   - requestAnimationFrame handler runs
+//     - Second requestAnimationFrame()
+//   - Style, layout, paint
+//   - IntersectionObserver generates a batch of notifications
+//     - Posts a task to deliver notifications
+// - Task to deliver IntersectionObserver notifications runs
+//   - IntersectionObserver callbacks run
+// - BeginFrame
+//   - Second requestAnimationFrameHandler runs
+//     - step_timeout()
+//   - IntersectionObserver generates another batch of notifications
+//     - Post task to deliver notifications
+// - step_timeout handler runs
+//   - myTestFunction1()
+//     - At this point, observer.takeRecords will get the second batch of
+//       notifications.
+function waitForNotification(t, f) {
+  requestAnimationFrame(function() {
+    requestAnimationFrame(function() { t.step_timeout(f, 0); });
+  });
+}
+
+// If you need to wait until the IntersectionObserver algorithm has a chance
+// to run, but don't need to wait for delivery of the notifications...
+function waitForFrame(t, f) {
+  requestAnimationFrame(function() {
+    t.step_timeout(f, 0);
+  });
+}
+
+// The timing of when runTestCycle is called is important.  It should be
+// called:
+//
+//   - Before or during the window load event, or
+//   - Inside of a prior runTestCycle callback, *before* any assert_* methods
+//     are called.
+//
+// Following these rules will ensure that the test suite will not abort before
+// all test steps have run.
+//
+// If the 'delay' parameter to the IntersectionObserver constructor is used,
+// tests will need to add the same delay to their runTestCycle invocations, to
+// wait for notifications to be generated and delivered.
+function runTestCycle(f, description, delay) {
+  async_test(function(t) {
+    if (delay) {
+      step_timeout(() => {
+        waitForNotification(t, t.step_func_done(f));
+      }, delay);
+    } else {
+      waitForNotification(t, t.step_func_done(f));
+    }
+  }, description);
+}
+
+// Root bounds for a root with an overflow clip as defined by:
+//   http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle
+function contentBounds(root) {
+  var left = root.offsetLeft + root.clientLeft;
+  var right = left + root.clientWidth;
+  var top = root.offsetTop + root.clientTop;
+  var bottom = top + root.clientHeight;
+  return [left, right, top, bottom];
+}
+
+// Root bounds for a root without an overflow clip as defined by:
+//   http://wicg.github.io/IntersectionObserver/#intersectionobserver-root-intersection-rectangle
+function borderBoxBounds(root) {
+  var left = root.offsetLeft;
+  var right = left + root.offsetWidth;
+  var top = root.offsetTop;
+  var bottom = top + root.offsetHeight;
+  return [left, right, top, bottom];
+}
+
+function clientBounds(element) {
+  var rect = element.getBoundingClientRect();
+  return [rect.left, rect.right, rect.top, rect.bottom];
+}
+
+function rectArea(rect) {
+  return (rect.left - rect.right) * (rect.bottom - rect.top);
+}
+
+function checkRect(actual, expected, description, all) {
+  if (!expected.length)
+    return;
+  assert_equals(actual.left | 0, expected[0] | 0, description + '.left');
+  assert_equals(actual.right | 0, expected[1] | 0, description + '.right');
+  assert_equals(actual.top | 0, expected[2] | 0, description + '.top');
+  assert_equals(actual.bottom | 0, expected[3] | 0, description + '.bottom');
+}
+
+function checkLastEntry(entries, i, expected) {
+  assert_equals(entries.length, i + 1, 'entries.length');
+  if (expected) {
+    checkRect(
+        entries[i].boundingClientRect, expected.slice(0, 4),
+        'entries[' + i + '].boundingClientRect', entries[i]);
+    checkRect(
+        entries[i].intersectionRect, expected.slice(4, 8),
+        'entries[' + i + '].intersectionRect', entries[i]);
+    checkRect(
+        entries[i].rootBounds, expected.slice(8, 12),
+        'entries[' + i + '].rootBounds', entries[i]);
+    if (expected.length > 12) {
+      assert_equals(
+          entries[i].isIntersecting, expected[12],
+          'entries[' + i + '].isIntersecting');
+    }
+  }
+}
+
+function checkJsonEntry(actual, expected) {
+  checkRect(
+      actual.boundingClientRect, expected.boundingClientRect,
+      'entry.boundingClientRect');
+  checkRect(
+      actual.intersectionRect, expected.intersectionRect,
+      'entry.intersectionRect');
+  if (actual.rootBounds == 'null')
+    assert_equals(expected.rootBounds, 'null', 'rootBounds is null');
+  else
+    checkRect(actual.rootBounds, expected.rootBounds, 'entry.rootBounds');
+  assert_equals(actual.isIntersecting, expected.isIntersecting);
+  assert_equals(actual.target, expected.target);
+}
+
+function checkJsonEntries(actual, expected, description) {
+  test(function() {
+    assert_equals(actual.length, expected.length);
+    for (var i = 0; i < actual.length; i++)
+      checkJsonEntry(actual[i], expected[i]);
+  }, description);
+}
+
+function checkIsIntersecting(entries, i, expected) {
+  assert_equals(entries[i].isIntersecting, expected,
+    'entries[' + i + '].target.isIntersecting equals ' + expected);
+}
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html
new file mode 100644
index 0000000..78f3d2c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/css/cssom-view/support/scroll-behavior.js"></script>
+<style>
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+<div class="spacer"></div>
+<iframe id="iframe"></iframe>
+<script>
+iframe.src =      // secure port
+  get_host_info().HTTPS_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html";
+
+window.addEventListener("message", async event => {
+  if (event.data == "scroll") {
+    iframe.scrollIntoView({ behavior: "instant" });
+    await waitForScrollEnd(document.scrollingElement);
+    window.parent.postMessage("scrollEnd", "*");
+  }
+});
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html
new file mode 100644
index 0000000..3676760
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<div id="target" style="height: 100px; background-color: green;"></div>
+<script>
+const observer = new IntersectionObserver(records => {
+  records.forEach(record => {
+    if (record.isIntersecting) {
+      window.top.postMessage(record.isIntersecting, "*");
+    }
+  });
+}, {});
+observer.observe(target);
+window.addEventListener("load", () => {
+  window.top.postMessage("ready", "*");
+});
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/observer-in-iframe-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/observer-in-iframe-subframe.html
new file mode 100644
index 0000000..9d0769a
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/observer-in-iframe-subframe.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./intersection-observer-test-utils.js"></script>
+
+<style>
+#root {
+  width: 200px;
+  height: 200px;
+}
+#scroller {
+  width: 160px;
+  height: 200px;
+  overflow-y: scroll;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.spacer {
+  height: 300px;
+}
+</style>
+
+<div id="root">
+  <div id="scroller">
+    <div class="spacer"></div>
+    <div id="target"></div>
+    <div class="spacer"></div>
+  </div>
+</div>
+
+<script>
+setup({message_events: [], output_document: window.parent.document});
+
+var entries = [];
+var root, scroller, target;
+
+runTestCycle(function() {
+  root = document.getElementById("root");
+  assert_true(!!root, "Root element exists.");
+  scroller = document.getElementById("scroller");
+  assert_true(!!scroller, "Scroller element exists.");
+  target = document.getElementById("target");
+  assert_true(!!target, "Target element exists.");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.")
+  runTestCycle(step1, "First rAF.");
+}, "IntersectionObserver in iframe with explicit root.");
+
+function step1() {
+  scroller.scrollTop = 250;
+  runTestCycle(step2, "scroller.scrollTop = 250");
+  checkLastEntry(entries, 0, [8, 108, 308, 408, 0, 0, 0, 0, 8, 208, 8, 208, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 1, [8, 108, 58, 158, 8, 108, 58, 158, 8, 208, 8, 208, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/same-origin-grand-child-iframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/same-origin-grand-child-iframe.html
new file mode 100644
index 0000000..25db5a2
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/same-origin-grand-child-iframe.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<div id="target"></div>
+<script>
+const observer = new IntersectionObserver(records => {
+  window.top.postMessage(records[0].rootBounds, "*");
+}, {});
+observer.observe(target);
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/scaled-target-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/scaled-target-subframe.html
new file mode 100644
index 0000000..8f6f930
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/scaled-target-subframe.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<style>
+html, body {
+  margin: 0;
+}
+#target {
+  width: 100px;
+  height: 100px;
+}
+</style>
+
+<div id="target">target</div>
+
+<script>
+var delay = 100;
+var results = [];
+
+function waitForNotification(f) {
+  setTimeout(() => {
+    requestAnimationFrame(function () {
+      requestAnimationFrame(function () {
+        setTimeout(f)
+      })
+    })
+  }, delay)
+}
+
+window.addEventListener("message", event => {
+  waitForNotification(() => {
+    window.parent.postMessage(results.map(e => e.isVisible), "*");
+    results = [];
+  });
+});
+
+onload = () => {
+  var target = document.getElementById("target");
+  var observer = new IntersectionObserver(entries => {
+    results = entries;
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(document.getElementById("target"));
+  window.parent.postMessage("", "*");
+};
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/timestamp-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/timestamp-subframe.html
new file mode 100644
index 0000000..143e4f6
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/timestamp-subframe.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<style>
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.spacer {
+  width: height: 100px
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+document.createObserverCallback = function(entries) {
+  return function(newEntries) {
+    for (var i in newEntries) {
+      entries.push(newEntries[i]);
+    }
+  };
+}
+document.createObserver = function(callback) {
+  return new IntersectionObserver(callback, {});
+};
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/resources/v2-subframe.html b/src/third_party/web_platform_tests/intersection-observer/resources/v2-subframe.html
new file mode 100644
index 0000000..295bbf0
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/resources/v2-subframe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<div id="target">target</div>
+<script>
+var delay = 100;
+var results = [];
+
+function waitForNotification(f) {
+  setTimeout(() => {
+    requestAnimationFrame(function () {
+      requestAnimationFrame(function () {
+        setTimeout(f)
+      })
+    })
+  }, delay)
+}
+
+window.addEventListener("message", event => {
+  waitForNotification(() => {
+    window.parent.postMessage(results.map(e => e.isVisible), "*");
+    results = [];
+  });
+});
+
+onload = () => {
+  var target = document.getElementById("target");
+  var observer = new IntersectionObserver(entries => {
+    results = entries;
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(document.getElementById("target"));
+  window.parent.postMessage("", "*");
+};
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/root-margin-root-element.html b/src/third_party/web_platform_tests/intersection-observer/root-margin-root-element.html
new file mode 100644
index 0000000..6016d45
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/root-margin-root-element.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 200px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="root">
+  <div style="height: 300px;"></div>
+  <div id="target"></div>
+</div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { root: root, rootMargin: "10px 20% 40% 30px" });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Root margin with explicit root.");
+
+function step0() {
+  document.scrollingElement.scrollTop = vh;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = window.innerHeight.");
+  checkLastEntry(entries, 0, [ 11, 111, vh + 411, vh + 511, 0, 0, 0, 0, -19, 131, vh + 101, vh + 391, false]);
+}
+
+function step1() {
+  root.scrollTop = 50;
+  runTestCycle(step2, "root.scrollTop = 50, putting target into root margin");
+  assert_equals(entries.length, 1, "No notifications after scrolling frame.");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 0.");
+  checkLastEntry(entries, 1, [11, 111, 361, 461, 11, 111, 361, 391, -19, 131, 101, 391, true]);
+}
+
+function step3() {
+  root.scrollTop = 0;
+  runTestCycle(step4, "root.scrollTop = 0");
+  checkLastEntry(entries, 1);
+}
+
+function step4() {
+  root.scrollTop = 50;
+  runTestCycle(step5, "root.scrollTop = 50 with root scrolled out of view.");
+  checkLastEntry(entries, 2, [ 11, 111, vh + 411, vh + 511, 0, 0, 0, 0, -19, 131, vh + 101, vh + 391, false]);
+}
+
+// This tests that notifications are generated even when the root element is off screen.
+function step5() {
+  checkLastEntry(entries, 3, [11, 111, vh + 361, vh + 461, 11, 111, vh + 361, vh + 391, -19, 131, vh + 101, vh + 391, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/root-margin-rounding.html b/src/third_party/web_platform_tests/intersection-observer/root-margin-rounding.html
new file mode 100644
index 0000000..f5e3323
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/root-margin-rounding.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<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://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1553673">
+<style>
+  html { width: 100vw; height: 100vh }
+</style>
+<script>
+const t = async_test("IntersectionObserver root margin cannot end up with negative rect (and thus non-intersecting) due to rounding");
+
+let remainingTests = 100;
+
+// This is just a best-effort test to catch issues.
+for (let i = 0; i < 100; ++i) {
+  let offset = i / 100;
+  let observer;
+  observer = new IntersectionObserver(t.step_func(function(entries) {
+    assert_equals(entries.length, 1);
+    assert_equals(entries[0].target, document.documentElement);
+    assert_true(entries[0].isIntersecting, "should be intersecting at " + offset);
+    if (!--remainingTests)
+      t.done();
+    observer.disconnect();
+  }), { rootMargin: `${-100 * (1 - offset)}% 0px ${-100 * offset}%` });
+  observer.observe(document.documentElement);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/root-margin.html b/src/third_party/web_platform_tests/intersection-observer/root-margin.html
new file mode 100644
index 0000000..898454c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/root-margin.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  display: inline-block;
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+.vertical-spacer {
+  height: calc(100vh + 100px);
+}
+.horizontal-spacer {
+  display: inline-block;
+  width: 120vw;
+}
+</style>
+
+<div class="vertical-spacer"></div>
+<div style="white-space:nowrap;">
+  <div class="horizontal-spacer"></div>
+  <div id="target"></div>
+  <div class="horizontal-spacer"></div>
+</div>
+<div class="vertical-spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { rootMargin: "10px 20% 40% 30px" });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Root margin tests");
+
+function step0() {
+  var targetBounds = clientBounds(target);
+  document.scrollingElement.scrollLeft = 100;
+  runTestCycle(step1, "document.scrollingElement.scrollLeft = 100");
+  checkLastEntry(entries, 0, targetBounds.concat(0, 0, 0, 0, -30, vw * 1.2, -10, vh * 1.4, false));
+}
+
+function step1() {
+  var targetBounds = clientBounds(target);
+  var sw = window.innerWidth - document.documentElement.clientWidth;
+  var sh = window.innerHeight - document.documentElement.clientHeight;
+  document.scrollingElement.scrollTop = vh + 200;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = document.documentElement.clientHeight + 200");
+  checkLastEntry(entries, 1, targetBounds.concat(
+    targetBounds[0], Math.min(targetBounds[1], vw * 1.2), vh + 108 + sh, Math.min(vh + 208 + sw, vh * 1.4),
+    -30, vw * 1.2, -10, vh * 1.4,
+    true
+  ));
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = vh + 300;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = document.documentElement.clientHeight + 300");
+  checkLastEntry(entries, 1);
+}
+
+function step3() {
+  var targetBounds = clientBounds(target);
+  document.scrollingElement.scrollLeft = 0;
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, targetBounds.concat(0, 0, 0, 0, -30, vw * 1.2, -10, vh * 1.4, false));
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/rtl-clipped-root.html b/src/third_party/web_platform_tests/intersection-observer/rtl-clipped-root.html
new file mode 100644
index 0000000..a30c6e3
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/rtl-clipped-root.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html dir="rtl">
+<head>
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="./resources/intersection-observer-test-utils.js"></script>
+
+  <style>
+  pre, #log {
+    position: absolute;
+    top: 120px;
+    left: 0;
+  }
+  #root {
+    width: 350px;
+    height: 100px;
+    border: 1px solid black;
+    display: flex;
+    flex-direction: row;
+    overflow-x: auto;
+  }
+  #target-start, #target-end {
+    width: 100px;
+    height: 100px;
+    flex-shrink: 0;
+    background-color: green;
+    text-align: center;
+  }
+  #target-end {
+    margin-inline-start: 500px;
+  }
+  </style>
+</head>
+
+<div id="root">
+  <div id="target-start">start</div>
+  <div id="target-end">end</div>
+</div>
+
+<script>
+runTestCycle(function() {
+  let io = new IntersectionObserver(entries => {
+    entries.forEach(entry => {
+      if (entry.isIntersecting) {
+        entry.target.classList.add("intersecting");
+      } else {
+        entry.target.classList.remove("intersecting");
+      }
+    });
+  }, { root: document.getElementById("root") });
+  document.querySelectorAll("#root > div").forEach(element => {
+    io.observe(element);
+  });
+  runTestCycle(step0, "First rAF");
+}, "Explicit rtl root with overflow clipping");
+
+function step0() {
+  assert_true(
+    document.getElementById("target-start").classList.contains("intersecting"),
+    "Target at scroll start is intersecting");
+  assert_false(
+    document.getElementById("target-end").classList.contains("intersecting"),
+    "Target at scroll end is not intersecting");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html b/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html
new file mode 100644
index 0000000..63e9f86
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-no-root.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver in a single document using the implicit root.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  checkLastEntry(entries, 0, [8, 108, vh + 108, vh + 208, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 108, vh - 192, vh - 92, 8, 108, vh - 192, vh - 92, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, [8, 108, vh + 8, vh + 108, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-root.html b/src/third_party/web_platform_tests/intersection-observer/same-document-root.html
new file mode 100644
index 0000000..bfb9b72
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-root.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#root {
+  display: inline-block;
+  overflow-y: scroll;
+  height: 200px;
+  border: 3px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="root">
+  <div style="height: 300px;"></div>
+  <div id="target"></div>
+</div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var root, target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, { root: root });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "IntersectionObserver in a single document with explicit root.");
+
+function step0() {
+  document.scrollingElement.scrollTop = vh;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = window.innerHeight.");
+  checkLastEntry(entries, 0, [ 11, 111, vh + 411, vh + 511, 0, 0, 0, 0, 11, 111, vh + 111, vh + 311, false]);
+}
+
+function step1() {
+  root.scrollTop = 150;
+  runTestCycle(step2, "root.scrollTop = 150 with root scrolled into view.");
+  assert_equals(entries.length, 1, "No notifications after scrolling frame.");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  runTestCycle(step3, "document.scrollingElement.scrollTop = 0.");
+  checkLastEntry(entries, 1, [11, 111, 261, 361, 11, 111, 261, 311, 11, 111, 111, 311, true]);
+}
+
+function step3() {
+  root.scrollTop = 0;
+  runTestCycle(step4, "root.scrollTop = 0");
+  checkLastEntry(entries, 1);
+}
+
+function step4() {
+  root.scrollTop = 150;
+  runTestCycle(step5, "root.scrollTop = 150 with root scrolled out of view.");
+  checkLastEntry(entries, 2, [11, 111, vh + 411, vh + 511, 0, 0, 0, 0, 11, 111, vh + 111, vh + 311, false]);
+}
+
+// This tests that notifications are generated even when the root element is off screen.
+function step5() {
+  checkLastEntry(entries, 3, [11, 111, vh + 261, vh + 361, 11, 111, vh + 261, vh + 311, 11, 111, vh + 111, vh + 311, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html b/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html
new file mode 100644
index 0000000..20bd11d
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-document-zero-size-target.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+#target {
+  width: 0px;
+  height: 0px;
+  background-color: green;
+}
+</style>
+
+<div class="spacer"></div>
+<div id="target"></div>
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "Target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF");
+}, "Observing a zero-area target.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  checkLastEntry(entries, 0, [8, 8, vh + 108, vh + 108, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 8, vh - 192, vh - 192, 8, 8, vh - 192, vh - 192, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, [8, 8, vh + 8, vh + 8, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/same-origin-grand-child-iframe.sub.html b/src/third_party/web_platform_tests/intersection-observer/same-origin-grand-child-iframe.sub.html
new file mode 100644
index 0000000..57c0347
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/same-origin-grand-child-iframe.sub.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+<iframe id="iframe"></iframe>
+<script>
+promise_test(async t => {
+  iframe.src =
+    get_host_info().HTTP_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/cross-origin-child-iframe.sub.html";
+
+  const rootBounds = await new Promise(resolve => {
+    window.addEventListener("message", event => resolve(event.data));
+  }, { once: true } );
+
+  assert_equals(rootBounds.left, 0);
+  assert_equals(rootBounds.top, 0);
+  assert_equals(rootBounds.right, document.documentElement.clientWidth);
+  assert_equals(rootBounds.bottom, document.documentElement.clientHeight);
+}, "rootBounds in a same-origin iframe in the case where there is a cross-origin "
++  "iframe in between the top document and the same origin iframe");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/shadow-content.html b/src/third_party/web_platform_tests/intersection-observer/shadow-content.html
new file mode 100644
index 0000000..ce9473c
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/shadow-content.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+</style>
+
+<div id="host"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  var shadowHost = document.getElementById("host");
+  assert_true(!!shadowHost, "Host exists");
+  var shadowRoot = shadowHost.attachShadow({ mode: "open" });
+  assert_true(!!shadowRoot, "Shadow root exists");
+  shadowRoot.innerHTML = "<div id='target' style='width: 100px; height: 100px; background-color: green;'></div>";
+  target = shadowRoot.getElementById("target");
+  assert_true(!!target, "target exists");
+
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF after creating shadow DOM.");
+}, "Observing a target inside shadow DOM.");
+
+function step0() {
+  checkLastEntry(entries, 0, [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/target-in-different-window.html b/src/third_party/web_platform_tests/intersection-observer/target-in-different-window.html
new file mode 100644
index 0000000..645b7ec
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/target-in-different-window.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<script>
+var entries = [];
+var popup, target;
+
+function waitForPopupNotification(f) {
+  popup.requestAnimationFrame(function() {
+    popup.requestAnimationFrame(function() { popup.setTimeout(f); });
+  });
+}
+
+async_test((t) => {
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes);
+  });
+  popup = window.open();
+  t.add_cleanup(() => popup.close());
+  target = popup.document.createElement('div');
+  target.style.width = "100px";
+  target.style.height = "100px";
+  observer.observe(target);
+  waitForPopupNotification(t.step_func(() => {
+    assert_equals(entries.length, 1, "Initial notification for detached target.");
+    assert_equals(entries[0].isIntersecting, false, "not intersecting");
+    popup.document.body.appendChild(target);
+    waitForPopupNotification(t.step_func_done(() => {
+      assert_equals(entries.length, 2, "Notification after insertion into popup.");
+      assert_equals(entries[1].isIntersecting, true, "intersecting");
+    }));
+  }));
+}, "IntersectionObserver with target in a different window.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/text-target.html b/src/third_party/web_platform_tests/intersection-observer/text-target.html
new file mode 100644
index 0000000..1abe535
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/text-target.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+</style>
+
+<div class="spacer"></div>
+<br id="target">
+<div class="spacer"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+var tw, th;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  let target_rect = target.getBoundingClientRect();
+  tw = target_rect.width;
+  th = target_rect.height;
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver observing a br element.");
+
+function step0() {
+  document.scrollingElement.scrollTop = 300;
+  runTestCycle(step1, "document.scrollingElement.scrollTop = 300");
+  // The numbers in brackets are target client rect; intersection rect;
+  // and root bounds.
+  checkLastEntry(entries, 0, [8, 8 + tw, vh + 108, vh + 108 + th, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+
+function step1() {
+  document.scrollingElement.scrollTop = 100;
+  runTestCycle(step2, "document.scrollingElement.scrollTop = 100");
+  checkLastEntry(entries, 1, [8, 8 + tw, vh - 192, vh - 192 + th, 8, 8 + tw, vh - 192, vh - 192 + th, 0, vw, 0, vh, true]);
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  checkLastEntry(entries, 2, [8, 8 + tw, vh + 8, vh + 8 + th, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/timestamp.html b/src/third_party/web_platform_tests/intersection-observer/timestamp.html
new file mode 100644
index 0000000..3f573bc
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/timestamp.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.spacer {
+  height: calc(100vh + 100px);
+}
+
+</style>
+<div id="leading-space" class="spacer"></div>
+<div id="trailing-space" class="spacer"></div>
+
+<script>
+// Pick this number to be comfortably greater than the length of two frames at 60Hz.
+var timeSkew = 40;
+
+var topWindowEntries = [];
+var iframeWindowEntries = [];
+var targetIframe;
+var topWindowTimeBeforeNotification;
+var iframeWindowTimeBeforeNotification;
+
+async_test(function(t) {
+  t.step_timeout(function() {
+    targetIframe = document.createElement("iframe");
+    assert_true(!!targetIframe, "iframe exists");
+    targetIframe.src = "resources/timestamp-subframe.html";
+    var trailingSpace = document.getElementById("trailing-space");
+    assert_true(!!trailingSpace, "trailing-space exists");
+    trailingSpace.parentNode.insertBefore(targetIframe, trailingSpace);
+    targetIframe.onload = function() {
+      var target = targetIframe.contentDocument.getElementById("target");
+      var iframeScroller = targetIframe.contentDocument.scrollingElement;
+
+      // Observer created here, callback created in iframe context.  Timestamps should be
+      // from this window.
+      var observer = new IntersectionObserver(
+          targetIframe.contentDocument.createObserverCallback(topWindowEntries), {});
+      assert_true(!!observer, "Observer exists");
+      observer.observe(target);
+
+      // Callback created here, observer created in iframe.  Timestamps should be
+      // from iframe window.
+      observer = targetIframe.contentDocument.createObserver(function(newEntries) {
+        iframeWindowEntries = iframeWindowEntries.concat(newEntries);
+      });
+      observer.observe(target);
+      runTestCycle(step1, "First rAF after iframe is loaded.");
+      t.done();
+    };
+  }, timeSkew);
+}, "Check that timestamps correspond to the to execution context that created the observer.");
+
+function step1() {
+  document.scrollingElement.scrollTop = 200;
+  targetIframe.contentDocument.scrollingElement.scrollTop = 250;
+  topWindowTimeBeforeNotification = performance.now();
+  iframeWindowTimeBeforeNotification = targetIframe.contentWindow.performance.now();
+  runTestCycle(step2, "Generate notifications.");
+  assert_equals(topWindowEntries.length, 1, "One notification to top window observer.");
+  assert_equals(iframeWindowEntries.length, 1, "One notification to iframe observer.");
+}
+
+function step2() {
+  document.scrollingElement.scrollTop = 0;
+  var topWindowTimeAfterNotification = performance.now();
+  var iframeWindowTimeAfterNotification = targetIframe.contentWindow.performance.now();
+
+  assert_approx_equals(
+      topWindowEntries[1].time - topWindowTimeBeforeNotification,
+      iframeWindowEntries[1].time - iframeWindowTimeBeforeNotification,
+      // Since all intersections are computed in a tight loop between 2 frames,
+      // an epsilon of 16ms (the length of one frame at 60Hz) turned out to be
+      // reliable, even at slow frame rates.
+      16,
+      "Notification times are relative to the expected time origins");
+
+  assert_equals(topWindowEntries.length, 2, "Top window observer has two notifications.");
+  assert_between_inclusive(
+      topWindowEntries[1].time,
+      topWindowTimeBeforeNotification,
+      topWindowTimeAfterNotification,
+      "Notification to top window observer is within the expected range.");
+
+  assert_equals(iframeWindowEntries.length, 2, "Iframe observer has two notifications.");
+  assert_between_inclusive(
+      iframeWindowEntries[1].time,
+      iframeWindowTimeBeforeNotification,
+      iframeWindowTimeAfterNotification,
+      "Notification to iframe observer is within the expected range.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/unclipped-root.html b/src/third_party/web_platform_tests/intersection-observer/unclipped-root.html
new file mode 100644
index 0000000..a59105e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/unclipped-root.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#root {
+  overflow: visible;
+  height: 200px;
+  width: 160px;
+  border: 7px solid black;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<div id="root">
+  <div id="target" style="transform: translateY(300px)"></div>
+</div>
+
+<script>
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var root = document.getElementById("root");
+  assert_true(!!root, "root exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {root: root});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "Test that border bounding box is used to calculate intersection with a non-scrolling root.");
+
+function step0() {
+  target.style.transform = "translateY(195px)";
+  runTestCycle(step1, "target.style.transform = 'translateY(195px)'");
+  checkLastEntry(entries, 0, [15, 115, 315, 415, 0, 0, 0, 0, 8, 182, 8, 222, false]);
+}
+
+function step1() {
+  target.style.transform = "";
+  checkLastEntry(entries, 1, [15, 115, 210, 310, 15, 115, 210, 222, 8, 182, 8, 222, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/animated-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/animated-occlusion.html
new file mode 100644
index 0000000..fa69733
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/animated-occlusion.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+@keyframes rotate {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(45deg);
+  }
+}
+#occluder {
+  will-change: transform;
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an animated occluding element.", delay);
+
+function step0() {
+  occluder.style.animation = "rotate .1s linear";
+  step_timeout(() => {
+    runTestCycle(step1, "occluder.style.animation = 'rotate .1s linear'", delay);
+  }, 50);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, vw, 0, vh, true, true]);
+}
+
+function step1() {
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, vw, 0, vh, true, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/blur-filter.html b/src/third_party/web_platform_tests/intersection-observer/v2/blur-filter.html
new file mode 100644
index 0000000..8cf6306
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/blur-filter.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  margin-top: 10px;
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+  filter: blur(50px);
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+
+  // First notification should report occlusion due to blur filter.
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/box-shadow.html b/src/third_party/web_platform_tests/intersection-observer/v2/box-shadow.html
new file mode 100644
index 0000000..765fa8b
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/box-shadow.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 100px;
+  height: 100px;
+  border: 0;
+}
+#box-shadow {
+  display: inline-block;
+  box-shadow: -50px -50px 0 50px rgba(255, 0, 0, 0.7);
+}
+</style>
+
+<iframe id=target srcdoc="<!DOCTYPE html><div>Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum.</div>"></iframe><div id=box-shadow></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("box-shadow");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  let observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing an iframe element.", delay);
+
+function step0() {
+  occluder.style.boxShadow = "none";
+  runTestCycle(step1, 'occluder.style.boxShadow = "none"', delay);
+  assert_equals(entries.length, 1, "Initial notification.");
+  assert_equals(entries[0].isVisible, false, "Initially occluded.");
+}
+
+function step1() {
+  occluder.style.boxShadow = "";
+  runTestCycle(step2, 'occluder.style.boxShadow = ""', delay);
+  assert_equals(entries.length, 2, "Notification after removing box shadow.");
+  assert_equals(entries[1].isVisible, true, "Visible when box shadow removed.");
+}
+
+function step2() {
+  assert_equals(entries.length, 3, "Notification after re-adding box shadow.");
+  assert_equals(entries[2].isVisible, false, "Occluded when box shadow re-added.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-effects.sub.html b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-effects.sub.html
new file mode 100644
index 0000000..5f328be
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-effects.sub.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+</style>
+
+<div id="container">
+  <iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/intersection-observer/resources/v2-subframe.html"></iframe>
+</div>
+
+<script>
+async_test(function(t) {
+  let container = document.getElementById("container");
+  let iframe = document.querySelector("iframe");
+
+  function step0(event) {
+    assert_equals(event.data,"");
+  }
+
+  function step1(event) {
+    container.style.opacity = "0.99";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step2(event) {
+    container.style.opacity = "";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  function step3(event) {
+    container.style.transform = "skew(30deg)";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step4(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  let steps = [step0, step1, step2, step3, step4];
+
+  window.addEventListener("message", event => {
+    if (steps.length) {
+      t.step(steps.shift(), t, event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*")
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "Intersection observer V2 test with visual effects on iframe.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-occlusion.sub.html b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-occlusion.sub.html
new file mode 100644
index 0000000..4c2f286
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/cross-origin-occlusion.sub.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 300px;
+  height: 150px;
+  border: none;
+}
+#occluder {
+  will-change: transform;
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+</style>
+
+<iframe src="http://{{domains[www1]}}:{{ports[http][0]}}/intersection-observer/resources/v2-subframe.html"></iframe>
+<div id="occluder"></div>
+
+<script>
+async_test(function(t) {
+  let iframe = document.querySelector("iframe");
+  let occluder = document.getElementById("occluder");
+
+  function step0(event) {
+    assert_equals(event.data,"");
+  }
+
+  function step1(event) {
+    occluder.style.marginTop = "-150px";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step2(event) {
+    occluder.style.marginTop = "";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  function step3(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  let steps = [step0, step1, step2, step3];
+
+  window.addEventListener("message", event => {
+    if (steps.length) {
+      t.step(steps.shift(), t, event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*");
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "Intersection observer V2 test with occlusion of target in iframe.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/delay-test.html b/src/third_party/web_platform_tests/intersection-observer/v2/delay-test.html
new file mode 100644
index 0000000..e3906ea
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/delay-test.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+async_test(t => {
+  let entries = [];
+  let delay = 100;
+  let target = document.getElementById("target");
+  let occluder = document.getElementById("occluder");
+
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  let observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  // The first notification should be sent without delay.
+  waitForNotification(t, t.step_func(step0));
+
+  function waitForDelay(timerExpiredBeforeLastFrame, nextStep) {
+    requestAnimationFrame(t.step_func(() => {
+      if (timerExpiredBeforeLastFrame) {
+        // New notifications should have been generated during the previous
+        // frame and delivered by now.
+        assert_equals(entries.length, 2);
+        assert_greater_than(entries[1].time - entries[0].time, delay);
+        assert_false(entries[1].isVisible);
+        nextStep();
+      } else {
+        // Observer may not have updated yet. Wait for next frame.
+        let timerExpired = performance.now() - entries[0].time >= delay;
+        waitForDelay(timerExpired, nextStep);
+      }
+    }));
+  }
+
+  function step0() {
+    assert_equals(entries.length, 1);
+    assert_true(entries[0].isVisible);
+    // This should trigger a notification on the next run.
+    occluder.style.marginTop = "-10px";
+    // Enter a rAF loop until the delay timer expires.
+    waitForDelay(false, step1);
+  }
+
+  function step1() {
+    occluder.style.marginTop = "10px";
+    // This style invalidation should cause a frame to run before the observer
+    // can generate a notification (due to delay parameter). Make sure the
+    // notification will still be generated even if we don't force more frames
+    // with a rAF loop.
+    t.step_timeout(() => {
+      assert_equals(entries.length, 3);
+      assert_true(entries[0].isVisible);
+      t.done();
+    }, 2 * delay);
+  }
+
+}, "'delay' parameter throttles frequency of notifications.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html b/src/third_party/web_platform_tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html
new file mode 100644
index 0000000..fc5b145
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 150px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+  float: left;
+}
+#occluder {
+  float: left;
+  margin-left: 10px;
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+  filter: drop-shadow(-50px 0);
+  writing-mode: vertical-rl;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+
+  // First notification should report occlusion due to drop shadow filter.
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/iframe-target.html b/src/third_party/web_platform_tests/intersection-observer/v2/iframe-target.html
new file mode 100644
index 0000000..53fbff8
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/iframe-target.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 150px;
+  height: 100px;
+  border: 0;
+}
+</style>
+
+<iframe srcdoc="<!DOCTYPE html><div>Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum.</div>"></iframe>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.querySelector("iframe");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing an iframe element.", delay);
+
+function step0() {
+  checkLastEntry(entries, 0, [0, 150, 0, 100, 0, 150, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/inline-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/inline-occlusion.html
new file mode 100644
index 0000000..e4b097e
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/inline-occlusion.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.testdiv {
+  font-size: 24px;
+}
+</style>
+
+<div class="testdiv">This is the <span id="target">target</span>.</div>
+<div class="testdiv" id="occluder">This is the occluder.</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  occluder.style.marginTop = "-10px";
+  runTestCycle(step1, "occluder.style.marginTop = '-10px'", delay);
+  assert_equals(entries.length, 1);
+  assert_true(entries[0].isVisible);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  assert_equals(entries.length, 2);
+  assert_false(entries[1].isVisible);
+}
+
+function step2() {
+  assert_equals(entries.length, 3);
+  assert_true(entries[2].isVisible);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/position-relative.html b/src/third_party/web_platform_tests/intersection-observer/v2/position-relative.html
new file mode 100644
index 0000000..4cdc429
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/position-relative.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.relpos {
+  position: relative;
+}
+</style>
+
+<div id="target" class="relpos">
+  <div class="relpos">
+    <img border="0" width="100" height="100" src=""/>
+  </div>
+</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing a position:relative div containing a position:relative child");
+
+function step0() {
+  assert_equals(entries.length, 1, "First notification.");
+  assert_true(entries[0].isVisible, "Target is visible.");
+}
+</script>
+
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/scaled-target.html b/src/third_party/web_platform_tests/intersection-observer/v2/scaled-target.html
new file mode 100644
index 0000000..f48f079
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/scaled-target.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#iframe {
+  width: 100px;
+  height: 100px;
+  border: 0;
+  margin-bottom: 10px;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+  position: relative;
+}
+</style>
+
+<iframe id="iframe" src="../resources/scaled-target-subframe.html"></iframe>
+<div id="occluder"></div>
+
+<script>
+async_test(function(t) {
+  let iframe = document.getElementById("iframe");
+
+  function step0(event) {
+    assert_equals(event.data, "");
+  }
+
+  function step1(event) {
+    iframe.style.transform = "scale(2)";
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([true]));
+  }
+
+  function step2(event) {
+    assert_equals(JSON.stringify(event.data),
+                  JSON.stringify([false]));
+  }
+
+  let steps = [step0, step1, step2];
+
+  window.addEventListener("message", event => {
+    if (steps.length) {
+      t.step(steps.shift(), t, event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*")
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "IntersectionObserver V2 test with scale applied to target.");
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/simple-effects.html b/src/third_party/web_platform_tests/intersection-observer/v2/simple-effects.html
new file mode 100644
index 0000000..baf3220
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/simple-effects.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#effects {
+  opacity: 1;
+  filter: none;
+}
+</style>
+
+<div id="effects">
+  <div id="target"></div>
+</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var effects;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  effects = document.getElementById("effects");
+  assert_true(!!target, "target exists");
+  assert_true(!!effects, "effects exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with a non-zero opacity ancestor.", delay);
+
+function step0() {
+  effects.style.opacity = "0.99";
+  runTestCycle(step1, "effects.style.opacity = 0.99", delay);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step1() {
+  effects.style.opacity = "1";
+  runTestCycle(step2, "effects.style.opacity = 1", delay);
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  effects.style.filter = "grayscale(50%)";
+  runTestCycle(step3, "effects.style.filter = grayscale(50%)", delay);
+  checkLastEntry(entries, 2, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step3() {
+  checkLastEntry(entries, 3, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html
new file mode 100644
index 0000000..588ec2a
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<svg id="svg" style="display: block">
+  <foreignObject>
+    <div id="occluder"></div>
+  </foreignObject>
+</svg>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  svg.style.marginTop = "-10px";
+  runTestCycle(step1, "svg.style.marginTop = '-10px'", delay);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  svg.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 2, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion.html
new file mode 100644
index 0000000..f3ce518
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/simple-occlusion.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 100px;
+  height: 100px;
+  background-color: green;
+}
+#occluder {
+  width: 100px;
+  height: 100px;
+  background-color: blue;
+}
+</style>
+
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  occluder.style.marginTop = "-10px";
+  runTestCycle(step1, "occluder.style.marginTop = '-10px'", delay);
+  checkLastEntry(entries, 0, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  checkLastEntry(entries, 1, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, false]);
+}
+
+function step2() {
+  checkLastEntry(entries, 2, [0, 100, 0, 100, 0, 100, 0, 100, 0, 800, 0, 600, true, true]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/text-editor-occlusion.html b/src/third_party/web_platform_tests/intersection-observer/v2/text-editor-occlusion.html
new file mode 100644
index 0000000..2edb7bb
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/text-editor-occlusion.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+.testdiv {
+  font-size: 24px;
+}
+</style>
+
+<div class="testdiv">Target: <input id="target" type="text"></input></div>
+<div class="testdiv" id="occluder">This is the occluder.</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("occluder");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 in a single document using the implicit root, with an occluding element.", delay);
+
+function step0() {
+  occluder.style.marginTop = "-10px";
+  runTestCycle(step1, "occluder.style.marginTop = '-10px'", delay);
+  assert_equals(entries.length, 1);
+  assert_true(entries[0].isVisible);
+}
+
+function step1() {
+  // Occluding elements with opacity=0 should not affect target visibility.
+  occluder.style.opacity = "0";
+  runTestCycle(step2, "occluder.style.opacity = 0", delay);
+  assert_equals(entries.length, 2);
+  assert_false(entries[1].isVisible);
+}
+
+function step2() {
+  assert_equals(entries.length, 3);
+  assert_true(entries[2].isVisible);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/v2/text-shadow.html b/src/third_party/web_platform_tests/intersection-observer/v2/text-shadow.html
new file mode 100644
index 0000000..cdfc1a2
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/v2/text-shadow.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../resources/intersection-observer-test-utils.js"></script>
+
+<style>
+body, html {
+  margin: 0;
+}
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+iframe {
+  width: 100px;
+  height: 100px;
+  border: 0;
+}
+#text-shadow {
+  display: inline-block;
+  font-size: 144px;
+  font-weight: 1000;
+  color: rgba(0, 0, 0, 0);
+  text-shadow: -100px 0 0 rgba(255, 0, 0, .7);
+}
+</style>
+
+<iframe id=target srcdoc="<!DOCTYPE html><div>Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum. Lorem ipsum.</div>"></iframe><div id=text-shadow>O</div>
+
+<script>
+var delay = 100;
+var entries = [];
+var target;
+var occluder;
+
+runTestCycle(function() {
+  target = document.getElementById("target");
+  occluder = document.getElementById("text-shadow");
+  assert_true(!!target, "target exists");
+  assert_true(!!occluder, "occluder exists");
+  let observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  }, {trackVisibility: true, delay: delay});
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.", delay);
+}, "IntersectionObserverV2 observing an iframe element.", delay);
+
+function step0() {
+  occluder.style.textShadow = "none";
+  runTestCycle(step1, 'occluder.style.textShadow = "none"', delay);
+  assert_equals(entries.length, 1, "Initial notification.");
+  assert_equals(entries[0].isVisible, false, "Initially occluded.");
+}
+
+function step1() {
+  occluder.style.textShadow = "";
+  runTestCycle(step2, 'occluder.style.textShadow = ""', delay);
+  assert_equals(entries.length, 2, "Notification after removing text shadow.");
+  assert_equals(entries[1].isVisible, true, "Visible when text shadow removed.");
+}
+
+function step2() {
+  assert_equals(entries.length, 3, "Notification after re-adding text shadow.");
+  assert_equals(entries[2].isVisible, false, "Occluded when text shadow re-added.");
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/zero-area-element-hidden.html b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-hidden.html
new file mode 100644
index 0000000..be57ac6
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-hidden.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 0px;
+  height: 0px;
+  position: fixed;
+  top: -1000px;
+}
+</style>
+
+<div id='target'></div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById('target');
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF.");
+}, "A zero-area hidden target should not be intersecting.");
+
+function step0() {
+  checkLastEntry(entries, 0, [8, 8, -1000, -1000, 0, 0, 0, 0, 0, vw, 0, vh, false]);
+}
+</script>
diff --git a/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html
new file mode 100644
index 0000000..5431750
--- /dev/null
+++ b/src/third_party/web_platform_tests/intersection-observer/zero-area-element-visible.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/intersection-observer-test-utils.js"></script>
+
+<style>
+pre, #log {
+  position: absolute;
+  top: 0;
+  left: 200px;
+}
+#target {
+  width: 0px;
+  height: 0px;
+}
+</style>
+
+<div id='target'></div>
+
+<script>
+var entries = [];
+
+runTestCycle(function() {
+  var target = document.getElementById('target');
+  assert_true(!!target, "target exists");
+  var observer = new IntersectionObserver(function(changes) {
+    entries = entries.concat(changes)
+  });
+  observer.observe(target);
+  entries = entries.concat(observer.takeRecords());
+  assert_equals(entries.length, 0, "No initial notifications.");
+  runTestCycle(step0, "First rAF should generate a notification.");
+}, "Ensure that a zero-area target intersecting root generates a notification with intersectionRatio == 1");
+
+function step0() {
+  assert_equals(entries.length, 1, "One notification.");
+  assert_equals(entries[0].intersectionRatio, 1, "intersectionRatio == 1");
+}
+</script>