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