summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/intersection-observer/resources
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/intersection-observer/resources')
-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
12 files changed, 671 insertions, 0 deletions
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>