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