|
44 | 44 | }""" |
45 | 45 |
|
46 | 46 |
|
| 47 | +# Scrolls the container and flags when the resulting scroll *event* is |
| 48 | +# delivered. shinychat updates its pinned/unpinned state inside its own scroll |
| 49 | +# handler, and scroll events are dispatched asynchronously (on Firefox, tied to |
| 50 | +# paint ticks that can lag under CI load) — so waiting on scrollTop alone races |
| 51 | +# the event delivery. Our once-listener registers after shinychat's, so by the |
| 52 | +# time the flag is set, shinychat has already processed the same event. |
| 53 | +SCROLL_TO_SCRIPT = """(args) => { |
| 54 | + const el = document.querySelector(args.selector); |
| 55 | + delete el.__testScrollEventSeen; |
| 56 | + el.addEventListener( |
| 57 | + "scroll", |
| 58 | + () => { el.__testScrollEventSeen = true; }, |
| 59 | + { once: true }, |
| 60 | + ); |
| 61 | + el.scrollTo(0, args.to === "bottom" ? el.scrollHeight : 0); |
| 62 | +}""" |
| 63 | + |
| 64 | + |
| 65 | +def scroll_to_and_wait_for_scroll_event(page: Page, selector: str, to: str) -> None: |
| 66 | + page.evaluate(SCROLL_TO_SCRIPT, {"selector": selector, "to": to}) |
| 67 | + page.wait_for_function( |
| 68 | + """(sel) => document.querySelector(sel).__testScrollEventSeen === true""", |
| 69 | + arg=selector, |
| 70 | + timeout=30_000, |
| 71 | + ) |
| 72 | + |
| 73 | + |
47 | 74 | def scroll_state(page: Page, selector: str) -> dict[str, float]: |
48 | 75 | return page.evaluate( |
49 | 76 | """(sel) => { |
@@ -120,20 +147,17 @@ def test_auto_scroll_pinning(page: Page, local_app: ShinyAppProc) -> None: |
120 | 147 | expect_not_scrolled(page, UNPINNED) |
121 | 148 |
|
122 | 149 | # Phase B: scrolling away from the bottom unpins; new content must NOT |
123 | | - # drag the container back down. |
124 | | - page.evaluate(f"""() => document.querySelector({PINNED!r}).scrollTo(0, 0)""") |
125 | | - page.wait_for_function( |
126 | | - f"""() => document.querySelector({PINNED!r}).scrollTop === 0""" |
127 | | - ) |
| 150 | + # drag the container back down. Waiting for the scroll event (not just |
| 151 | + # scrollTop) guarantees shinychat has registered the unpin before the next |
| 152 | + # chunk arrives. |
| 153 | + scroll_to_and_wait_for_scroll_event(page, PINNED, "top") |
128 | 154 | next_chunk_and_wait_for_render(page, next_chunk, 4) |
129 | 155 | expect_not_scrolled(page, PINNED) |
130 | 156 |
|
131 | 157 | # Phase C: scrolling back to the bottom re-pins; the next chunk is |
132 | | - # followed again. |
133 | | - page.evaluate(f"""() => {{ |
134 | | - const el = document.querySelector({PINNED!r}); |
135 | | - el.scrollTo(0, el.scrollHeight); |
136 | | - }}""") |
| 158 | + # followed again. Same rationale: shinychat only re-pins inside its scroll |
| 159 | + # handler, so wait for the event to be delivered before streaming more. |
| 160 | + scroll_to_and_wait_for_scroll_event(page, PINNED, "bottom") |
137 | 161 | expect_at_bottom(page, PINNED) |
138 | 162 | next_chunk_and_wait_for_render(page, next_chunk, 5) |
139 | 163 | expect_at_bottom(page, PINNED) |
|
0 commit comments