summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/intersection-observer
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/intersection-observer')
-rw-r--r--testing/web-platform/tests/intersection-observer/META.yml3
-rw-r--r--testing/web-platform/tests/intersection-observer/bounding-box.html85
-rw-r--r--testing/web-platform/tests/intersection-observer/callback-cross-realm-report-exception.html30
-rw-r--r--testing/web-platform/tests/intersection-observer/client-rect.html46
-rw-r--r--testing/web-platform/tests/intersection-observer/clip-path.html77
-rw-r--r--testing/web-platform/tests/intersection-observer/containing-block.html74
-rw-r--r--testing/web-platform/tests/intersection-observer/cross-document-root.html49
-rw-r--r--testing/web-platform/tests/intersection-observer/cross-origin-iframe.sub.html53
-rw-r--r--testing/web-platform/tests/intersection-observer/disconnect.html54
-rw-r--r--testing/web-platform/tests/intersection-observer/display-none.html68
-rw-r--r--testing/web-platform/tests/intersection-observer/document-scrolling-element-root.html50
-rw-r--r--testing/web-platform/tests/intersection-observer/edge-inclusive-intersection.html67
-rw-r--r--testing/web-platform/tests/intersection-observer/empty-root-margin.html29
-rw-r--r--testing/web-platform/tests/intersection-observer/explicit-root-different-document.tentative.html27
-rw-r--r--testing/web-platform/tests/intersection-observer/idlharness.window.js22
-rw-r--r--testing/web-platform/tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html74
-rw-r--r--testing/web-platform/tests/intersection-observer/iframe-no-root.html72
-rw-r--r--testing/web-platform/tests/intersection-observer/initial-observation-with-threshold.html61
-rw-r--r--testing/web-platform/tests/intersection-observer/inline-client-rect.html93
-rw-r--r--testing/web-platform/tests/intersection-observer/inline-with-block-child-client-rect.html46
-rw-r--r--testing/web-platform/tests/intersection-observer/intersection-ratio-ib-split.html45
-rw-r--r--testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-2.html37
-rw-r--r--testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-in-iframe.html32
-rw-r--r--testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds.html42
-rw-r--r--testing/web-platform/tests/intersection-observer/isIntersecting-change-events.html113
-rw-r--r--testing/web-platform/tests/intersection-observer/isIntersecting-threshold.html54
-rw-r--r--testing/web-platform/tests/intersection-observer/multiple-targets.html81
-rw-r--r--testing/web-platform/tests/intersection-observer/multiple-thresholds.html97
-rw-r--r--testing/web-platform/tests/intersection-observer/nested-cross-origin-iframe.sub.html75
-rw-r--r--testing/web-platform/tests/intersection-observer/not-in-containing-block-chain.tentative.html25
-rw-r--r--testing/web-platform/tests/intersection-observer/observer-attributes.html41
-rw-r--r--testing/web-platform/tests/intersection-observer/observer-callback-arguments.html28
-rw-r--r--testing/web-platform/tests/intersection-observer/observer-exceptions.html61
-rw-r--r--testing/web-platform/tests/intersection-observer/observer-in-iframe.html10
-rw-r--r--testing/web-platform/tests/intersection-observer/observer-without-js-reference.html51
-rw-r--r--testing/web-platform/tests/intersection-observer/remove-element.html83
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/cross-origin-child-iframe.sub.html7
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/cross-origin-subframe.html171
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/iframe-no-root-subframe.html4
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/intersection-observer-test-utils.js217
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/intersection-ratio-with-fractional-bounds-in-iframe-content.html50
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html22
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html15
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/observer-in-iframe-subframe.html65
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/same-origin-grand-child-iframe.html17
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/scaled-target-subframe.html43
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/timestamp-subframe.html28
-rw-r--r--testing/web-platform/tests/intersection-observer/resources/v2-subframe.html32
-rw-r--r--testing/web-platform/tests/intersection-observer/root-margin-root-element.html90
-rw-r--r--testing/web-platform/tests/intersection-observer/root-margin-rounding.html31
-rw-r--r--testing/web-platform/tests/intersection-observer/root-margin.html87
-rw-r--r--testing/web-platform/tests/intersection-observer/rtl-clipped-root.html66
-rw-r--r--testing/web-platform/tests/intersection-observer/same-document-no-root.html62
-rw-r--r--testing/web-platform/tests/intersection-observer/same-document-root.html91
-rw-r--r--testing/web-platform/tests/intersection-observer/same-document-with-document-root.html60
-rw-r--r--testing/web-platform/tests/intersection-observer/same-document-zero-size-target.html62
-rw-r--r--testing/web-platform/tests/intersection-observer/same-origin-grand-child-iframe.sub.html33
-rw-r--r--testing/web-platform/tests/intersection-observer/shadow-content.html45
-rw-r--r--testing/web-platform/tests/intersection-observer/target-in-detached-document.html48
-rw-r--r--testing/web-platform/tests/intersection-observer/target-in-different-window.html41
-rw-r--r--testing/web-platform/tests/intersection-observer/target-is-root.html35
-rw-r--r--testing/web-platform/tests/intersection-observer/text-target.html63
-rw-r--r--testing/web-platform/tests/intersection-observer/timestamp.html100
-rw-r--r--testing/web-platform/tests/intersection-observer/unclipped-root.html58
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/animated-occlusion.html73
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/blur-filter.html65
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/box-shadow.html67
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/cross-origin-effects.sub.html65
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/cross-origin-occlusion.sub.html69
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/delay-test.html89
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html66
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/iframe-target.html45
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/inline-occlusion.html62
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/position-absolute-overflow-visible-and-not-visible.html51
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/position-relative.html49
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/scaled-target.html62
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/simple-effects.html72
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html71
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/simple-occlusion.html67
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/text-editor-occlusion.html62
-rw-r--r--testing/web-platform/tests/intersection-observer/v2/text-shadow.html70
-rw-r--r--testing/web-platform/tests/intersection-observer/zero-area-element-hidden.html44
-rw-r--r--testing/web-platform/tests/intersection-observer/zero-area-element-visible.html45
83 files changed, 4792 insertions, 0 deletions
diff --git a/testing/web-platform/tests/intersection-observer/META.yml b/testing/web-platform/tests/intersection-observer/META.yml
new file mode 100644
index 0000000000..31dddab561
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/IntersectionObserver/
+suggested_reviewers:
+ - szager-chromium
diff --git a/testing/web-platform/tests/intersection-observer/bounding-box.html b/testing/web-platform/tests/intersection-observer/bounding-box.html
new file mode 100644
index 0000000000..367243d558
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/callback-cross-realm-report-exception.html b/testing/web-platform/tests/intersection-observer/callback-cross-realm-report-exception.html
new file mode 100644
index 0000000000..0bec720485
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/callback-cross-realm-report-exception.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>IntersectionObserver reports the exception from its callback in the callback's global object</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<iframe srcdoc='<div style="height: 100px;">foo</div>'></iframe>
+<iframe></iframe>
+<iframe></iframe>
+<script>
+setup({ allow_uncaught_exception: true });
+
+const onerrorCalls = [];
+window.onerror = () => { onerrorCalls.push("top"); };
+frames[0].onerror = () => { onerrorCalls.push("frame0"); };
+frames[1].onerror = () => { onerrorCalls.push("frame1"); };
+frames[2].onerror = () => { onerrorCalls.push("frame2"); };
+
+async_test(t => {
+ window.onload = t.step_func(() => {
+ const target = frames[0].document.querySelector("div");
+ const io = new frames[0].IntersectionObserver(new frames[1].Function(`throw new parent.frames[2].Error("PASS");`));
+ io.observe(target);
+
+ t.step_timeout(() => {
+ assert_array_equals(onerrorCalls, ["frame1"]);
+ t.done();
+ }, 25);
+ });
+});
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/client-rect.html b/testing/web-platform/tests/intersection-observer/client-rect.html
new file mode 100644
index 0000000000..e85171ca7c
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/clip-path.html b/testing/web-platform/tests/intersection-observer/clip-path.html
new file mode 100644
index 0000000000..a43d3fb7b0
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/clip-path.html
@@ -0,0 +1,77 @@
+<!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 { margin: 0 }
+pre, #log {
+ position: absolute;
+ top: 0;
+ left: 200px;
+}
+#target {
+ background-color: green;
+ width: 100px;
+ height: 100px;
+}
+#container {
+ padding: 8px;
+ width: 0px;
+ height: 0px;
+}
+</style>
+
+<div id="container">
+ <div id="target"></div>
+</div>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+
+promise_test(async function(t) {
+ var target = document.getElementById("target");
+ var container = document.getElementById("container");
+ 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.");
+
+ await waitForNotification();
+
+ checkLastEntry(
+ entries,
+ 0,
+ [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true],
+ "IntersectionObserver notification after first rAF",
+ );
+ container.style.clipPath = "inset(1000px)";
+
+ await waitForNotification();
+
+ checkLastEntry(
+ entries,
+ 1,
+ [8, 108, 8, 108, 0, 0, 0, 0, 0, vw, 0, vh, false],
+ "IntersectionObserver should send a not-intersecting notification for a target that gets fully clipped by clip-path.",
+ );
+
+ container.style.clipPath = "";
+
+ await waitForNotification();
+
+ checkLastEntry(
+ entries,
+ 2,
+ [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true],
+ "Intersecting notification after removing display:none on target.",
+ );
+});
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/containing-block.html b/testing/web-platform/tests/intersection-observer/containing-block.html
new file mode 100644
index 0000000000..f7ce6fa724
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/cross-document-root.html b/testing/web-platform/tests/intersection-observer/cross-document-root.html
new file mode 100644
index 0000000000..9288bf5806
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/cross-document-root.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<link rel="author" href="mailto:szager@chromium.org" title="Stefan Zager">
+<link rel="help" href="https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo">
+
+<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: document });
+ 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, observing a target in a same-origin iframe.");
+};
+
+function step0() {
+ checkLastEntry(entries, 0, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false]);
+}
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/cross-origin-iframe.sub.html b/testing/web-platform/tests/intersection-observer/cross-origin-iframe.sub.html
new file mode 100644
index 0000000000..d444237f6b
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/disconnect.html b/testing/web-platform/tests/intersection-observer/disconnect.html
new file mode 100644
index 0000000000..9c02dafabe
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/display-none.html b/testing/web-platform/tests/intersection-observer/display-none.html
new file mode 100644
index 0000000000..6c2040ee0c
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/display-none.html
@@ -0,0 +1,68 @@
+<!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 = [];
+
+promise_test(async function(t) {
+ 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.");
+
+ await waitForNotification();
+
+ checkLastEntry(
+ entries,
+ 0,
+ [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true],
+ "IntersectionObserver notification after first rAF",
+ );
+ target.style.display = "none";
+
+ await waitForNotification();
+
+ checkLastEntry(
+ entries,
+ 1,
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false],
+ "IntersectionObserver should send a not-intersecting notification for a target that gets display:none.",
+ );
+
+ target.style.display = "";
+
+ await waitForNotification();
+
+ checkLastEntry(
+ entries,
+ 2,
+ [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true],
+ "Intersecting notification after removing display:none on target.",
+ );
+});
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/document-scrolling-element-root.html b/testing/web-platform/tests/intersection-observer/document-scrolling-element-root.html
new file mode 100644
index 0000000000..443ff2ed85
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/edge-inclusive-intersection.html b/testing/web-platform/tests/intersection-observer/edge-inclusive-intersection.html
new file mode 100644
index 0000000000..b73c407a27
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/empty-root-margin.html b/testing/web-platform/tests/intersection-observer/empty-root-margin.html
new file mode 100644
index 0000000000..9eaf856e95
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/explicit-root-different-document.tentative.html b/testing/web-platform/tests/intersection-observer/explicit-root-different-document.tentative.html
new file mode 100644
index 0000000000..15c5e620d5
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/explicit-root-different-document.tentative.html
@@ -0,0 +1,27 @@
+<!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="help" href="https://github.com/w3c/IntersectionObserver/issues/457">
+<style>
+ div {
+ width: 100px;
+ height: 100px;
+ background: blue;
+ margin: 10px
+ }
+</style>
+<div id="root"></div>
+<script>
+let t = async_test("IntersectionObserver reports a (non-intersecting) entry if different-document from the doc");
+let doc = document.implementation.createHTMLDocument("");
+let target = doc.createElement("div");
+doc.body.appendChild(target);
+new IntersectionObserver(
+ t.step_func_done(function(records) {
+ assert_equals(records.length, 1);
+ assert_false(records[0].isIntersecting);
+ }),
+ { root: document.querySelector("#root") }
+).observe(target);
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/idlharness.window.js b/testing/web-platform/tests/intersection-observer/idlharness.window.js
new file mode 100644
index 0000000000..2059e1ce63
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html b/testing/web-platform/tests/intersection-observer/iframe-no-root-with-wrapping-scroller.html
new file mode 100644
index 0000000000..28e6d09d7b
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/iframe-no-root.html b/testing/web-platform/tests/intersection-observer/iframe-no-root.html
new file mode 100644
index 0000000000..8532246fd1
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/initial-observation-with-threshold.html b/testing/web-platform/tests/intersection-observer/initial-observation-with-threshold.html
new file mode 100644
index 0000000000..b9218b09ea
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/inline-client-rect.html b/testing/web-platform/tests/intersection-observer/inline-client-rect.html
new file mode 100644
index 0000000000..c096230eb1
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/inline-with-block-child-client-rect.html b/testing/web-platform/tests/intersection-observer/inline-with-block-child-client-rect.html
new file mode 100644
index 0000000000..81a8fd1256
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/intersection-ratio-ib-split.html b/testing/web-platform/tests/intersection-observer/intersection-ratio-ib-split.html
new file mode 100644
index 0000000000..ba5370e335
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/intersection-ratio-ib-split.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez">
+<link rel="author" href="https://mozilla.org" title="Mozilla">
+<link rel="help" href="https://w3c.github.io/IntersectionObserver/#dom-intersectionobserverentry-intersectionratio">
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1581876">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+ block {
+ display: block;
+ width: 50vw;
+ height: 50vh;
+ background: green;
+ }
+</style>
+<inline>
+ <block></block>
+</inline>
+<script>
+
+// Account for rounding differences when viewport sizes can't cleanly divide.
+const epsilon = 1;
+
+promise_test(async function() {
+ for (let element of document.querySelectorAll("inline, block")) {
+ let entries = await new Promise(resolve => {
+ new IntersectionObserver(resolve).observe(element);
+ });
+ assert_equals(entries.length, 1, element.nodeName + ": Should get an entry");
+ assert_true(entries[0].isIntersecting, element.nodeName + ": Should be intersecting");
+ assert_equals(entries[0].intersectionRatio, 1, element.nodeName + ": Should be fully intersecting");
+
+ function assert_rects_equal(r1, r2, label) {
+ assert_approx_equals(r1.top, r2.top, epsilon, label + ": top should be equal");
+ assert_approx_equals(r1.right, r2.right, epsilon, label + ": right should be equal");
+ assert_approx_equals(r1.bottom, r2.bottom, epsilon, label + ": bottom should be equal");
+ assert_approx_equals(r1.left, r2.left, epsilon, label + ": left should be equal");
+ }
+
+ assert_rects_equal(entries[0].boundingClientRect, element.getBoundingClientRect(), element.nodeName + ": boundingClientRect should match");
+ assert_rects_equal(entries[0].intersectionRect, entries[0].boundingClientRect, element.nodeName + ": intersectionRect should match entry.boundingClientRect");
+ }
+}, "IntersectionObserver on an IB split gets the right intersection ratio");
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-2.html b/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-2.html
new file mode 100644
index 0000000000..1e250accd8
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-2.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<title>IntersectionObserver ratio with fractional bounds</title>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1278897">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+ #container {
+ overflow: hidden;
+ width: 500px;
+ }
+ #target {
+ display: list-item;
+ padding: 16.1px;
+ background: rebeccapurple;
+ }
+</style>
+
+<div id="container">
+ <div id="list">
+ <span id="target"></span>
+ </div>
+</div>
+
+<script>
+async_test(function(t) {
+ let target = document.getElementById("target");
+ let list = document.getElementById("list");
+ let observer = new IntersectionObserver(t.step_func_done(function(entries) {
+ assert_equals(entries.length, 1);
+ assert_equals(entries[0].intersectionRatio, 1);
+ assert_equals(entries[0].isIntersecting, true);
+ }), { root: list, threshold: 1 });
+ observer.observe(target);
+});
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-in-iframe.html b/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-in-iframe.html
new file mode 100644
index 0000000000..b00f8ddaf6
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds-in-iframe.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ .iframe-container {
+ width: 100%;
+ border: 0;
+ height: 200px;
+ }
+ </style>
+</head>
+<body>
+<script>
+ const asyncTest = async_test("intersectionRatio in iframe should be 1 for totally visible target with fractional bounds");
+ const onIframeObserved = (event) => {
+ const ratio = event.detail.intersectionRatio;
+ asyncTest.step(() => {
+ assert_equals(ratio, 1);
+ });
+ asyncTest.done();
+ };
+ window.document.addEventListener("iframeObserved", onIframeObserved, false);
+</script>
+<iframe class="iframe-container" src="./resources/intersection-ratio-with-fractional-bounds-in-iframe-content.html"></iframe>
+</body>
+</html>
diff --git a/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds.html b/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds.html
new file mode 100644
index 0000000000..9f54bef976
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/intersection-ratio-with-fractional-bounds.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<title>IntersectionObserver ratio with fractional bounds</title>
+<link rel="help" href="https://bugs.chromium.org/p/chromium/issues/detail?id=1020466">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<style>
+ body {
+ margin: 0;
+ }
+ #container {
+ display: flex;
+ }
+ #left {
+ height: 100px;
+ width: 0.88875%;
+ background: lightblue;
+ }
+ #target {
+ height: 100px;
+ width: 99.11125%;
+ background: rebeccapurple;
+ }
+</style>
+
+<div id="container">
+ <div id="left"></div>
+ <div id="target"></div>
+</div>
+
+<script>
+async_test(function(t) {
+ let target = document.getElementById("target");
+ let observer = new IntersectionObserver(t.step_func_done(function(entries) {
+ assert_equals(entries.length, 1);
+ assert_equals(entries[0].intersectionRatio, 1);
+ assert_equals(entries[0].isIntersecting, true);
+ }));
+ observer.observe(target);
+});
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/isIntersecting-change-events.html b/testing/web-platform/tests/intersection-observer/isIntersecting-change-events.html
new file mode 100644
index 0000000000..99bc65bd60
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/isIntersecting-threshold.html b/testing/web-platform/tests/intersection-observer/isIntersecting-threshold.html
new file mode 100644
index 0000000000..842c8e2c9f
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/isIntersecting-threshold.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>
+#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);
+ // See https://github.com/w3c/IntersectionObserver/issues/432
+ assert_equals(entries[1].isIntersecting, false);
+ scroller.scrollTop = 100;
+}
+
+function step4() {
+ assert_equals(entries.length, 2);
+}
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/multiple-targets.html b/testing/web-platform/tests/intersection-observer/multiple-targets.html
new file mode 100644
index 0000000000..22353e3aaa
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/multiple-thresholds.html b/testing/web-platform/tests/intersection-observer/multiple-thresholds.html
new file mode 100644
index 0000000000..3599e1f722
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/nested-cross-origin-iframe.sub.html b/testing/web-platform/tests/intersection-observer/nested-cross-origin-iframe.sub.html
new file mode 100644
index 0000000000..090d236399
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/not-in-containing-block-chain.tentative.html b/testing/web-platform/tests/intersection-observer/not-in-containing-block-chain.tentative.html
new file mode 100644
index 0000000000..4490d0b631
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/not-in-containing-block-chain.tentative.html
@@ -0,0 +1,25 @@
+<!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="help" href="https://github.com/w3c/IntersectionObserver/issues/457">
+<style>
+ div {
+ width: 100px;
+ height: 100px;
+ background: blue;
+ margin: 10px
+ }
+</style>
+<div id="target"></div>
+<div id="root"></div>
+<script>
+let t = async_test("IntersectionObserver reports a (non-intersecting) entry even if not in the containing block chain");
+new IntersectionObserver(
+ t.step_func_done(function(records) {
+ assert_equals(records.length, 1);
+ assert_false(records[0].isIntersecting);
+ }),
+ { root: document.querySelector("#root") }
+).observe(document.querySelector("#target"));
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/observer-attributes.html b/testing/web-platform/tests/intersection-observer/observer-attributes.html
new file mode 100644
index 0000000000..9d0b788b33
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/observer-attributes.html
@@ -0,0 +1,41 @@
+<!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");
+
+ observer = new IntersectionObserver(function(e) {}, {
+ rootMargin: " ",
+ threshold: []
+ });
+ test(function() { assert_array_equals(observer.thresholds, [0]) },
+ "empty observer.thresholds");
+ test(function() { assert_equals(observer.rootMargin, "0px 0px 0px 0px") },
+ "whitespace 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/testing/web-platform/tests/intersection-observer/observer-callback-arguments.html b/testing/web-platform/tests/intersection-observer/observer-callback-arguments.html
new file mode 100644
index 0000000000..6e816969d0
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/observer-callback-arguments.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<meta charset=utf-8>
+<title>IntersectionObserver: callback arguments</title>
+<link rel="help" href="https://w3c.github.io/IntersectionObserver/#notify-intersection-observers-algo">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+"use strict";
+
+async_test(t => {
+ const io = new IntersectionObserver(function(entries, observer) {
+ t.step(() => {
+ assert_equals(this, io);
+ assert_equals(arguments.length, 2);
+ assert_true(Array.isArray(entries));
+ assert_equals(entries.length, 1);
+ assert_true(entries[0] instanceof IntersectionObserverEntry);
+ assert_equals(observer, io);
+
+ io.disconnect();
+ t.done();
+ });
+ });
+
+ io.observe(document.body);
+}, "Callback is invoked with |this| value of IntersectionObserver and two arguments");
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/observer-exceptions.html b/testing/web-platform/tests/intersection-observer/observer-exceptions.html
new file mode 100644
index 0000000000..126790f290
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/observer-in-iframe.html b/testing/web-platform/tests/intersection-observer/observer-in-iframe.html
new file mode 100644
index 0000000000..e918bf1a4f
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/observer-without-js-reference.html b/testing/web-platform/tests/intersection-observer/observer-without-js-reference.html
new file mode 100644
index 0000000000..53100c50bb
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/remove-element.html b/testing/web-platform/tests/intersection-observer/remove-element.html
new file mode 100644
index 0000000000..a093b22028
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/resources/cross-origin-child-iframe.sub.html b/testing/web-platform/tests/intersection-observer/resources/cross-origin-child-iframe.sub.html
new file mode 100644
index 0000000000..c341cd4102
--- /dev/null
+++ b/testing/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 scrolling="no" frameborder="0" id="iframe"></iframe>
+<script>
+iframe.src =
+ get_host_info().ORIGIN + "/intersection-observer/resources/same-origin-grand-child-iframe.html";
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/resources/cross-origin-subframe.html b/testing/web-platform/tests/intersection-observer/resources/cross-origin-subframe.html
new file mode 100644
index 0000000000..1b34d7c7b7
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/resources/cross-origin-subframe.html
@@ -0,0 +1,171 @@
+<!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 === document.documentElement ? "html" : entry.target.id
+ };
+}
+
+function boundingClientRectToJson(element) {
+ let r = element.getBoundingClientRect();
+ return [r.left, r.right, r.top, r.bottom];
+}
+
+// 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);
+observer.observe(document.documentElement);
+
+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
+ }, {
+ boundingClientRect: boundingClientRectToJson(document.documentElement),
+ intersectionRect: [0, 0, 0, 0],
+ rootBounds: "null",
+ isIntersecting: false,
+ target: "html"
+ }];
+ port.postMessage({
+ actual: entries.map(entryToJson),
+ expected: expected,
+ description: "First rAF"
+ }, "*");
+ entries = [];
+ port.postMessage({scrollTo: 200}, "*");
+}
+
+function step1() {
+ entries = entries.concat(observer.takeRecords());
+ var client_rect = boundingClientRectToJson(document.documentElement);
+ // When the top document is scrolled all the way up, the iframe element is
+ // 108px below the scrolling viewport, and the iframe has a 2px border. When
+ // the top document is scrolled to y=200, the top 90px of the iframe's content
+ // is visible.
+ var expected = [{
+ boundingClientRect: client_rect,
+ intersectionRect: client_rect.slice(0, 3).concat(90),
+ rootBounds: "null",
+ isIntersecting: true,
+ target: "html"
+ }];
+ port.postMessage({
+ actual: entries.map(entryToJson),
+ expected: 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
+ }, {
+ boundingClientRect: boundingClientRectToJson(document.documentElement),
+ intersectionRect: [0, 0, 0, 0],
+ rootBounds: "null",
+ isIntersecting: false,
+ target: "html"
+ }];
+ 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/testing/web-platform/tests/intersection-observer/resources/iframe-no-root-subframe.html b/testing/web-platform/tests/intersection-observer/resources/iframe-no-root-subframe.html
new file mode 100644
index 0000000000..ee63a06ca0
--- /dev/null
+++ b/testing/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/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
new file mode 100644
index 0000000000..c26ccea030
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/resources/intersection-observer-test-utils.js
@@ -0,0 +1,217 @@
+// 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) {
+ return new Promise(resolve => {
+ requestAnimationFrame(function() {
+ requestAnimationFrame(function() {
+ let callback = function() {
+ resolve();
+ if (f) {
+ f();
+ }
+ };
+ if (t) {
+ t.step_timeout(callback);
+ } else {
+ setTimeout(callback);
+ }
+ });
+ });
+ });
+}
+
+// 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) {
+ return new Promise(resolve => {
+ requestAnimationFrame(function() {
+ t.step_timeout(function() {
+ resolve();
+ if (f) {
+ f();
+ }
+ });
+ });
+ });
+}
+
+// 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/testing/web-platform/tests/intersection-observer/resources/intersection-ratio-with-fractional-bounds-in-iframe-content.html b/testing/web-platform/tests/intersection-observer/resources/intersection-ratio-with-fractional-bounds-in-iframe-content.html
new file mode 100644
index 0000000000..696ebf6ebe
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/resources/intersection-ratio-with-fractional-bounds-in-iframe-content.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <style>
+ body {
+ margin: 0;
+ height: 100%;
+ }
+ .app {
+ display: flex;
+ width: 100%;
+ }
+ .sidebar {
+ width: 31.1%;
+ margin: 0;
+ background: rgb(231, 249, 139);
+ }
+ section {
+ width: 68.9%;
+ background-color: rgb(119, 219, 172);
+ border: dashed red 3px;
+ }
+ p {
+ margin-top: 0;
+ }
+ </style>
+</head>
+<body>
+ <div class=app>
+ <div class="sidebar"></div>
+ <section id=target>
+ <h2>Observer target</h2>
+ <p><strong>Intersection ratio:</strong> <span id=ratio></span></p>
+ </section>
+ </div>
+
+ <script>
+ const onIntersection = entries => {
+ const ratio = entries[0].intersectionRatio;
+ const eventData = { intersectionRatio: ratio };
+ document.getElementById("ratio").innerText = ratio.toFixed(5);
+ const event = new CustomEvent("iframeObserved", {detail: eventData});
+ parent.document.dispatchEvent(event);
+ };
+ const observer = new IntersectionObserver(onIntersection, {threshold: 1});
+ setTimeout(() => {observer.observe(document.getElementById("target"))}, 0);
+ </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html b/testing/web-platform/tests/intersection-observer/resources/nested-cross-origin-child-iframe.sub.html
new file mode 100644
index 0000000000..78f3d2ca26
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html b/testing/web-platform/tests/intersection-observer/resources/nested-cross-origin-grand-child-iframe.html
new file mode 100644
index 0000000000..3676760e35
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/resources/observer-in-iframe-subframe.html b/testing/web-platform/tests/intersection-observer/resources/observer-in-iframe-subframe.html
new file mode 100644
index 0000000000..9d0769ae44
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/resources/same-origin-grand-child-iframe.html b/testing/web-platform/tests/intersection-observer/resources/same-origin-grand-child-iframe.html
new file mode 100644
index 0000000000..0a1426ef02
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/resources/same-origin-grand-child-iframe.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<!--
+ target should be fully vertically in-viewport as 100px is way less than the
+ default iframe height.
+
+ right: 0 makes sure that we're occluded by 8px by the intermediate iframe.
+-->
+<div id="target" style="width: 100px; height: 100px; position: absolute; right: 0"></div>
+<script>
+const observer = new IntersectionObserver(records => {
+ if (records[0].isIntersecting) {
+ let { rootBounds, intersectionRect } = records[0];
+ window.top.postMessage({ rootBounds, intersectionRect }, "*");
+ }
+}, {});
+observer.observe(document.getElementById("target"));
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/resources/scaled-target-subframe.html b/testing/web-platform/tests/intersection-observer/resources/scaled-target-subframe.html
new file mode 100644
index 0000000000..8f6f930e00
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/resources/timestamp-subframe.html b/testing/web-platform/tests/intersection-observer/resources/timestamp-subframe.html
new file mode 100644
index 0000000000..143e4f6e23
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/resources/v2-subframe.html b/testing/web-platform/tests/intersection-observer/resources/v2-subframe.html
new file mode 100644
index 0000000000..295bbf047e
--- /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>
diff --git a/testing/web-platform/tests/intersection-observer/root-margin-root-element.html b/testing/web-platform/tests/intersection-observer/root-margin-root-element.html
new file mode 100644
index 0000000000..6016d45bdf
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/root-margin-rounding.html b/testing/web-platform/tests/intersection-observer/root-margin-rounding.html
new file mode 100644
index 0000000000..f5e3323019
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/root-margin.html b/testing/web-platform/tests/intersection-observer/root-margin.html
new file mode 100644
index 0000000000..898454c4f3
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/rtl-clipped-root.html b/testing/web-platform/tests/intersection-observer/rtl-clipped-root.html
new file mode 100644
index 0000000000..a30c6e38c5
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/same-document-no-root.html b/testing/web-platform/tests/intersection-observer/same-document-no-root.html
new file mode 100644
index 0000000000..63e9f86d9c
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/same-document-root.html b/testing/web-platform/tests/intersection-observer/same-document-root.html
new file mode 100644
index 0000000000..bfb9b729aa
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/same-document-with-document-root.html b/testing/web-platform/tests/intersection-observer/same-document-with-document-root.html
new file mode 100644
index 0000000000..15cb7c4cbc
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/same-document-with-document-root.html
@@ -0,0 +1,60 @@
+<!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)
+ }, {root: document});
+ 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 document as 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 = 0;
+ checkLastEntry(entries, 1, [8, 108, vh - 192, vh - 92, 8, 108, vh - 192, vh - 92, 0, vw, 0, vh, true]);
+ }
+</script>
+
diff --git a/testing/web-platform/tests/intersection-observer/same-document-zero-size-target.html b/testing/web-platform/tests/intersection-observer/same-document-zero-size-target.html
new file mode 100644
index 0000000000..20bd11d4be
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/same-origin-grand-child-iframe.sub.html b/testing/web-platform/tests/intersection-observer/same-origin-grand-child-iframe.sub.html
new file mode 100644
index 0000000000..a311a8732a
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/same-origin-grand-child-iframe.sub.html
@@ -0,0 +1,33 @@
+<!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 scrolling="no" frameborder="0" 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, intersectionRect } = await new Promise(resolve => {
+ window.addEventListener("message", event => resolve(event.data));
+ }, { once: true } );
+
+ // 300px = iframe viewport width
+ // 8px = default body margin
+ // (intersectionRect is in the coordinate space of the target iframe)
+ assert_equals(intersectionRect.top, 8);
+ assert_equals(intersectionRect.left, 200);
+ assert_equals(intersectionRect.right, 300 - 8);
+ assert_equals(intersectionRect.width, 100 - 8);
+ assert_equals(intersectionRect.height, 100);
+
+ 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/testing/web-platform/tests/intersection-observer/shadow-content.html b/testing/web-platform/tests/intersection-observer/shadow-content.html
new file mode 100644
index 0000000000..ce9473cb79
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/target-in-detached-document.html b/testing/web-platform/tests/intersection-observer/target-in-detached-document.html
new file mode 100644
index 0000000000..6a5cf1e811
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/target-in-detached-document.html
@@ -0,0 +1,48 @@
+<!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: 100px;
+ height: 100px;
+ background-color: green;
+}
+</style>
+
+<script>
+var vw = document.documentElement.clientWidth;
+var vh = document.documentElement.clientHeight;
+
+var entries = [];
+var target;
+
+runTestCycle(function() {
+ var detached_document = document.implementation.createHTMLDocument("test");
+ target = detached_document.createElement("div");
+ target.id = "target";
+ var observer = new IntersectionObserver(function(changes) {
+ entries = entries.concat(changes)
+ });
+ observer.observe(target);
+ runTestCycle(step0, "First rAF.");
+}, "IntersectionObserver in a single document using the implicit root.");
+
+function step0() {
+ document.adoptNode(target);
+ document.body.appendChild(target);
+ checkLastEntry(entries, 0, [0, 0, 0, 0, 0, 0, 0, 0]);
+ runTestCycle(step1, "Adopt target.");
+}
+
+function step1() {
+ checkLastEntry(entries, 1, [8, 108, 8, 108, 8, 108, 8, 108, 0, vw, 0, vh, true]);
+}
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/target-in-different-window.html b/testing/web-platform/tests/intersection-observer/target-in-different-window.html
new file mode 100644
index 0000000000..fcf5ba9437
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/target-in-different-window.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<!--
+ NOTE(emilio): This tests Chrome's behavior but it's not clear that's what the
+ spec asks for, see https://github.com/w3c/IntersectionObserver/issues/456
+-->
+<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/testing/web-platform/tests/intersection-observer/target-is-root.html b/testing/web-platform/tests/intersection-observer/target-is-root.html
new file mode 100644
index 0000000000..1fe2ae6e17
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/target-is-root.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<title>IntersectionObserver when root == target doesn't compute an intersection</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1682915">
+<link rel="help" href="https://w3c.github.io/IntersectionObserver/#update-intersection-observations-algo">
+<!--
+ Quoting IntersectionObserver section 3.2.8, "Run the Update Intersection Observations Steps", step 2, substep 3:
+
+ If the intersection root is an Element, and target is not a descendant of
+ the intersection root in the containing block chain, skip to step 11.
+
+-->
+<style>
+ #container {
+ overflow: scroll;
+ width: 100px;
+ height: 100px;
+ }
+</style>
+<div id=container>
+ <div></div>
+</div>
+<script>
+async_test(function(t) {
+ let container = document.getElementById("container");
+ let observer = new IntersectionObserver(t.step_func_done(function(entries) {
+ assert_equals(entries.length, 1);
+ assert_equals(entries[0].intersectionRatio, 0);
+ assert_equals(entries[0].isIntersecting, false);
+ }), { root: container });
+ observer.observe(container);
+});
+</script>
diff --git a/testing/web-platform/tests/intersection-observer/text-target.html b/testing/web-platform/tests/intersection-observer/text-target.html
new file mode 100644
index 0000000000..1abe5357c2
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/timestamp.html b/testing/web-platform/tests/intersection-observer/timestamp.html
new file mode 100644
index 0000000000..3f573bcd88
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/unclipped-root.html b/testing/web-platform/tests/intersection-observer/unclipped-root.html
new file mode 100644
index 0000000000..a59105e33e
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/animated-occlusion.html b/testing/web-platform/tests/intersection-observer/v2/animated-occlusion.html
new file mode 100644
index 0000000000..fa69733b9f
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/blur-filter.html b/testing/web-platform/tests/intersection-observer/v2/blur-filter.html
new file mode 100644
index 0000000000..8cf63066e1
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/box-shadow.html b/testing/web-platform/tests/intersection-observer/v2/box-shadow.html
new file mode 100644
index 0000000000..765fa8b2d5
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/cross-origin-effects.sub.html b/testing/web-platform/tests/intersection-observer/v2/cross-origin-effects.sub.html
new file mode 100644
index 0000000000..5f328bec99
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/cross-origin-occlusion.sub.html b/testing/web-platform/tests/intersection-observer/v2/cross-origin-occlusion.sub.html
new file mode 100644
index 0000000000..4c2f286afb
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/delay-test.html b/testing/web-platform/tests/intersection-observer/v2/delay-test.html
new file mode 100644
index 0000000000..e3906ea2c2
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html b/testing/web-platform/tests/intersection-observer/v2/drop-shadow-filter-vertical-rl.html
new file mode 100644
index 0000000000..fc5b145e1f
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/iframe-target.html b/testing/web-platform/tests/intersection-observer/v2/iframe-target.html
new file mode 100644
index 0000000000..53fbff86b7
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/inline-occlusion.html b/testing/web-platform/tests/intersection-observer/v2/inline-occlusion.html
new file mode 100644
index 0000000000..e4b097e62a
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/position-absolute-overflow-visible-and-not-visible.html b/testing/web-platform/tests/intersection-observer/v2/position-absolute-overflow-visible-and-not-visible.html
new file mode 100644
index 0000000000..c74d4c2021
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/v2/position-absolute-overflow-visible-and-not-visible.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+<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>
+ html, body {
+ margin: 0;
+ padding: 0;
+ height: 200px;
+ width: 100%;
+ }
+ #parent {
+ position: relative;
+ height: 200px;
+ width: 200px;
+ background-color: rgb(250, 221, 221);
+ overflow-y: clip;
+ overflow-x: visible;
+ }
+ #child {
+ background-color: rgb(208, 250, 208);
+ position: absolute;
+ left: 100%;
+ width: 200px;
+ height: 200px;
+ }
+</style>
+</head>
+<body>
+<div id="parent">
+ <div id="child"></div>
+</div>
+</body>
+<script>
+const test = async_test("ParentWithOverflowVisibleAndNotVisible");
+const child = document.getElementById("child");
+const observer = new IntersectionObserver((entries) => {
+ test.step(() => {
+ assert_true(entries[0].isIntersecting);
+ assert_equals(entries[0].intersectionRatio, 1.0);
+ assert_equals(entries[0].intersectionRect.height, 200);
+ assert_equals(entries[0].intersectionRect.width, 200);
+ });
+ test.done();
+});
+observer.observe(child);
+</script>
+</html> \ No newline at end of file
diff --git a/testing/web-platform/tests/intersection-observer/v2/position-relative.html b/testing/web-platform/tests/intersection-observer/v2/position-relative.html
new file mode 100644
index 0000000000..4cdc429570
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/scaled-target.html b/testing/web-platform/tests/intersection-observer/v2/scaled-target.html
new file mode 100644
index 0000000000..f48f0792d0
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/simple-effects.html b/testing/web-platform/tests/intersection-observer/v2/simple-effects.html
new file mode 100644
index 0000000000..baf32203c7
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html b/testing/web-platform/tests/intersection-observer/v2/simple-occlusion-svg-foreign-object.html
new file mode 100644
index 0000000000..588ec2abd6
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/simple-occlusion.html b/testing/web-platform/tests/intersection-observer/v2/simple-occlusion.html
new file mode 100644
index 0000000000..f3ce518b34
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/text-editor-occlusion.html b/testing/web-platform/tests/intersection-observer/v2/text-editor-occlusion.html
new file mode 100644
index 0000000000..2edb7bbbe6
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/v2/text-shadow.html b/testing/web-platform/tests/intersection-observer/v2/text-shadow.html
new file mode 100644
index 0000000000..cdfc1a2d2a
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/zero-area-element-hidden.html b/testing/web-platform/tests/intersection-observer/zero-area-element-hidden.html
new file mode 100644
index 0000000000..be57ac6983
--- /dev/null
+++ b/testing/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/testing/web-platform/tests/intersection-observer/zero-area-element-visible.html b/testing/web-platform/tests/intersection-observer/zero-area-element-visible.html
new file mode 100644
index 0000000000..b012b65c18
--- /dev/null
+++ b/testing/web-platform/tests/intersection-observer/zero-area-element-visible.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;
+}
+#target {
+ width: 0px;
+ height: 0px;
+}
+#container {
+ overflow: clip;
+}
+</style>
+
+<div id="container">
+ <div id='target'></div>
+</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>