Bug 1526260 [wpt PR 15175] - [IntersectionObserver] Upstream V2 tests to wpt, a=testonly
authorStefan Zager <szager@chromium.org>
Tue, 05 Mar 2019 11:12:29 +0000
changeset 522422 3712f9fd213a880081984ca55447eae36057f4f5
parent 522421 4e826d648be4e6be0a756d7e06d2a4ad5605d28f
child 522423 24c31f79b5add4d2b053c5faaaf6fcdb1c123436
push id10871
push usercbrindusan@mozilla.com
push dateMon, 18 Mar 2019 15:49:32 +0000
treeherdermozilla-beta@018abdd16060 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1526260, 15175, 827639, 1447267, 631836
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1526260 [wpt PR 15175] - [IntersectionObserver] Upstream V2 tests to wpt, a=testonly Automatic update from web-platform-tests [IntersectionObserver] Upstream V2 tests to wpt BUG=827639 Change-Id: I38fcdd0ae40cd3122541ffa8521d4ee30adffc22 Reviewed-on: https://chromium-review.googlesource.com/c/1447267 Reviewed-by: Chris Harrelson <chrishtr@chromium.org> Reviewed-by: Philip J├Ągenstedt <foolip@chromium.org> Commit-Queue: Stefan Zager <szager@chromium.org> Cr-Commit-Position: refs/heads/master@{#631836} -- wpt-commits: 4122c04d5d5fb30cf69f5c8ca993d74162688884 wpt-pr: 15175
testing/web-platform/tests/intersection-observer/resources/intersection-observer-test-utils.js
testing/web-platform/tests/intersection-observer/resources/v2-subframe.html
testing/web-platform/tests/intersection-observer/v2/animated-occlusion.html
testing/web-platform/tests/intersection-observer/v2/blur-filter.html
testing/web-platform/tests/intersection-observer/v2/box-shadow.html
testing/web-platform/tests/intersection-observer/v2/cross-origin-effects.sub.html
testing/web-platform/tests/intersection-observer/v2/cross-origin-occlusion.sub.html
testing/web-platform/tests/intersection-observer/v2/delay-test.html
testing/web-platform/tests/intersection-observer/v2/iframe-target.html
testing/web-platform/tests/intersection-observer/v2/simple-effects.html
testing/web-platform/tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html
testing/web-platform/tests/intersection-observer/v2/simple-occlusion.html
testing/web-platform/tests/intersection-observer/v2/text-shadow.html
testing/web-platform/tests/lint.whitelist
--- a/testing/web-platform/tests/intersection-observer/resources/intersection-observer-test-utils.js
+++ b/testing/web-platform/tests/intersection-observer/resources/intersection-observer-test-utils.js
@@ -67,32 +67,50 @@
 //   - 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); });
+    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.
-function runTestCycle(f, description) {
+//
+// 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) {
-    waitForNotification(t, t.step_func_done(f));
+    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;
@@ -169,9 +187,9 @@ function checkJsonEntries(actual, expect
     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);
-}
\ No newline at end of file
+}
new file mode 100644
--- /dev/null
+++ b/testing/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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/animated-occlusion.html
@@ -0,0 +1,72 @@
+<!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;
+}
+#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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/blur-filter.html
@@ -0,0 +1,64 @@
+<!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;
+}
+#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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/box-shadow.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: 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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/cross-origin-effects.sub.html
@@ -0,0 +1,64 @@
+<!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;
+}
+</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_func(steps.shift(), event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*")
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "Intersection observer V2 test with visual effects on iframe.");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/cross-origin-occlusion.sub.html
@@ -0,0 +1,68 @@
+<!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: 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_func(steps.shift(), event);
+      waitForFrame(t, () => {
+        iframe.contentWindow.postMessage("", "*");
+      });
+    } else {
+      t.done();
+    }
+  });
+
+}, "Intersection observer V2 test with occlusion of target in iframe.");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/delay-test.html
@@ -0,0 +1,75 @@
+<!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;
+}
+#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) {
+    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);
+        t.done();
+      } else {
+        // Observer may not have updated yet. Wait for next frame.
+        let timerExpired = performance.now() - entries[0].time >= delay;
+        waitForDelay(timerExpired);
+      }
+    }));
+  }
+
+  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);
+  }
+
+}, "'delay' parameter throttles frequency of notifications.");
+</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/iframe-target.html
@@ -0,0 +1,44 @@
+<!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;
+}
+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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/simple-effects.html
@@ -0,0 +1,71 @@
+<!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;
+}
+#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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html
@@ -0,0 +1,70 @@
+<!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;
+}
+#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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/simple-occlusion.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: 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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/text-shadow.html
@@ -0,0 +1,69 @@
+<!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;
+}
+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>
--- a/testing/web-platform/tests/lint.whitelist
+++ b/testing/web-platform/tests/lint.whitelist
@@ -169,16 +169,17 @@ SET TIMEOUT: html/webappapis/dynamic-mar
 SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/resources/history-frame.html
 SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/resources/url-entry-document-timer-frame.html
 SET TIMEOUT: html/webappapis/dynamic-markup-insertion/opening-the-input-stream/tasks.window.js
 SET TIMEOUT: html/webappapis/scripting/event-loops/*
 SET TIMEOUT: html/webappapis/scripting/events/event-handler-processing-algorithm-error/*
 SET TIMEOUT: html/webappapis/scripting/processing-model-2/*
 SET TIMEOUT: IndexedDB/*
 SET TIMEOUT: infrastructure/*
+SET TIMEOUT: intersection-observer/resources/v2-subframe.html
 SET TIMEOUT: intersection-observer/target-in-different-window.html
 SET TIMEOUT: media-source/mediasource-util.js
 SET TIMEOUT: media-source/URL-createObjectURL-revoke.html
 SET TIMEOUT: mixed-content/generic/sanity-checker.js
 SET TIMEOUT: navigation-timing/*
 SET TIMEOUT: offscreen-canvas/the-offscreen-canvas/*
 SET TIMEOUT: old-tests/submission/Microsoft/history/history_000.htm
 SET TIMEOUT: paint-timing/resources/subframe-painting.html