1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
|
<!DOCTYPE html>
<meta charset=utf-8>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Scroll margin propagation from descendant frame to top page</title>
<link rel="author" title="Kiet Ho" href="mailto:kiet.ho@apple.com">
<meta name="timeout" content="long">
<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>
<!--
This tests that when
(1) an implicit root intersection observer includes a scroll margin
(2) the observer target is in a frame descendant of the top page
Then the scroll margin is applied up to, and excluding, the first cross-origin-domain
frame in the chain from the target to the top page. Then, subsequent frames won't
have scroll margin applied, even if any of subsequent frames are same-origin-domain.
This follows the discussion at [1] that says:
> Implementation notes:
> * [...]
> * Should stop margins at a cross-origin iframe boundary for security
[1]: https://github.com/w3c/IntersectionObserver/issues/431#issuecomment-1542502858
The setup:
* 3-level iframe nesting: top page -> iframe 1 -> iframe 2 -> iframe 3
* Iframe 1 is cross-origin-domain with top page, iframe 2/3 are same-origin-domain
* Top page and iframe 1/2 have a scroller, which consists of a spacer to trigger
scrolling, and an iframe to the next level.
* Iframe 3 has an implicit root intersection observer and the target.
* The observer specifies a scroll margin, which should be applied to iframe 2,
and not to iframe 1 and top page.
Communication between frames:
* Iframe 3 sends a "isIntersectingChanged" to the top page when the target's
isIntersecting changed.
* Iframe 1, 2 accepts a "setScrollTop" message to set the scrollTop of its scroller.
The message contains a destination, if the destination matches, it sets the scrollTop,
otherwise it passes the message down the chain. After setting scrollTop, the iframe emits
a "scrollEnd" message to the top frame.
-->
<p>Top page</p>
<div style="width: 400px; height: 400px; outline: 1px solid blue; overflow-y: scroll" id="scroller">
<!-- Spacer to trigger scrolling -->
<div style="height: 500px"></div>
<iframe width=350 height=400 id="iframe"></iframe>
</div>
<script>
iframe.src =
get_host_info().HTTP_NOTSAMESITE_ORIGIN + "/intersection-observer/resources/scroll-margin-propagation-iframe-1.html";
const iframeWindow = iframe.contentWindow;
// Set the scrollTop of the scroller in the frame specified by `target`:
// "this" - top frame, "iframe1" - iframe 1, "iframe2" - iframe2
// When setting scrollTop of remote frames, remote frame will send a "scrollEnd"
// message to indicate the scroll has been set. Wait for this message before returning.
async function setScrollTop(target, scrollTop) {
if (target === "this") {
scroller.scrollTop = scrollTop;
} else {
iframeWindow.postMessage({
msgName: "setScrollTop",
target: target,
scrollTop: scrollTop
}, "*");
await new Promise(resolve => {
window.addEventListener("message", event => {
if (event.data.msgName === "scrollEnd" && event.data.source === target)
resolve();
}, { once: true })
})
}
// Wait for IntersectionObserver notifications to be generated.
await new Promise(resolve => waitForNotification(null, resolve));
await new Promise(resolve => waitForNotification(null, resolve));
}
var grandchildFrameIsIntersecting = null;
promise_setup(() => {
// Wait for the initial IntersectionObserver notification.
// This indicates iframe 3 is fully ready for test.
return new Promise(resolve => {
window.addEventListener("message", event => {
if (event.data.msgName === "isIntersectingChanged") {
grandchildFrameIsIntersecting = event.data.value;
// Install a long-lasting event listener, since this listerner is one-shot
window.addEventListener("message", event => {
if (event.data.msgName === "isIntersectingChanged")
grandchildFrameIsIntersecting = event.data.value;
});
resolve();
}
}, { once: true });
});
});
promise_test(async t => {
// Scroll everything to bottom, so target is fully visible
await setScrollTop("this", 99999);
await setScrollTop("iframe1", 99999);
await setScrollTop("iframe2", 99999);
assert_true(grandchildFrameIsIntersecting, "Target is fully visible and intersecting");
// Scroll iframe 2 up a bit so that target is not visible, but still intersecting
// because of scroll margin.
await setScrollTop("iframe2", 130);
assert_true(grandchildFrameIsIntersecting, "Target is not visible, but in the scroll margin zone, so still intersects");
await setScrollTop("iframe2", 85);
assert_false(grandchildFrameIsIntersecting, "Target is fully outside the visible and scroll margin zone");
}, "Scroll margin is applied to iframe 2, because it's same-origin-domain with iframe 3");
promise_test(async t => {
// Scroll everything to bottom, so target is fully visible
await setScrollTop("this", 99999);
await setScrollTop("iframe1", 99999);
await setScrollTop("iframe2", 99999);
assert_true(grandchildFrameIsIntersecting, "Target is fully visible");
await setScrollTop("iframe1", 180);
assert_false(grandchildFrameIsIntersecting, "Target is not visible, in the scroll margin zone, but not intersecting because scroll margin doesn't apply to cross-origin-domain frames");
}, "Scroll margin is not applied to iframe 1, because it's cross-origin-domain with iframe 3");
promise_test(async t => {
// Scroll everything to bottom, so target is fully visible
await setScrollTop("this", 99999);
await setScrollTop("iframe1", 99999);
await setScrollTop("iframe2", 99999);
assert_true(grandchildFrameIsIntersecting, "Target is fully visible");
await setScrollTop("this", 235);
assert_false(grandchildFrameIsIntersecting, "Target is not visible, in the scroll margin zone, but not intersecting because scroll margin doesn't apply to frames beyond cross-origin-domain frames");
}, "Scroll margin is not applied to top page, because scroll margin doesn't propagate past cross-origin-domain iframe 1");
</script>
|