File: scroll-margin-propagation.html

package info (click to toggle)
firefox 147.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,683,324 kB
  • sloc: cpp: 7,607,156; javascript: 6,532,492; ansic: 3,775,158; python: 1,415,368; xml: 634,556; asm: 438,949; java: 186,241; sh: 62,751; makefile: 18,079; objc: 13,092; perl: 12,808; yacc: 4,583; cs: 3,846; pascal: 3,448; lex: 1,720; ruby: 1,003; php: 436; lisp: 258; awk: 247; sql: 66; sed: 54; csh: 10; exp: 6
file content (150 lines) | stat: -rw-r--r-- 6,376 bytes parent folder | download | duplicates (2)
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>