tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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>