diff options
Diffstat (limited to 'testing/web-platform/tests/intersection-observer/resources')
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> |