fix: make scroll restoration robust against slow/loaded content - #59
Merged
Merged
Conversation
Two issues made test_scroll_restoration_preserves_scroll flaky under CI load: 1. ScrollRestoration's restore loop used a fixed 10-animation-frame budget. After a client-side navigation the destination content is streamed in by the server, so the document can still be too short to reach the saved position, and frame cadence also collapses under CPU load. When the budget expired early, scrollTo clamped against a short/short-lived document and the position was silently dropped (observed: scroll stuck near 0). Replace the frame budget with a wall-clock window and stop as soon as the target is reached (within a small tolerance), so we keep re-asserting the position while late content grows, and never fight the user's own scroll. 2. The test navigated back with a synthetic element.click() and asserted a loose lower bound. A synthetic click can outrun the Link preventDefault handler, triggering a real anchor navigation (full page reload) that wipes the client-side scroll store; and the bottom-anchored forward link made Playwright scroll it into view, saving the page-bottom position so the lower-bound check passed without verifying real restoration. Use a native page.click (waits for actionability, so the handler is attached), place the forward link in-viewport at the target via a spacer so scroll-into-view is a no-op, return via the browser back action, and assert a near-exact restored position.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
test_scroll_restoration_preserves_scrollwas intermittently failing in CI with a scroll-restore timeout. Root-cause analysis (rAF/scrollTo tracing under CPU-pinned load) showed two independent defects — one in the component and one in the test.Underlying component bug
The
ScrollRestorationrestore loop used a fixed 10-animation-frame budget. After a client-side navigation the destination route's content is streamed in by the ReactPy server round-trip, so the document can still be too short to scroll to the saved position, and frame cadence additionally collapses under CPU load. When the frame budget expired early,window.scrollTo(pos)clamped against the not-yet-tall document and the restored position was silently dropped (observed live:scrollYstuck near the top while the restored value was never applied).Fix: replace the frame-count budget with a wall-clock window and stop the moment the target is reached (within a 2px tolerance). The loop keeps re-asserting the position while late content grows, and the early stop means it never fights the user's own scrolling. Effect cleanup cancels any in-flight loop so overlapping re-renders don't spawn competing loops.
Test methodology bug
The old test:
element.click()to navigate, which can outrun theLinkcomponent'spreventDefaulthandler. When that happens the anchor performs a real navigation (a full page reload) that wipes the client-side scroll store — reproducing the CIgot 0signature under load.>= 450assertion then passed for the wrong reason, never verifying the intended scroll was saved/restored.Fix:
page.click, which waits for actionability so the router's handler is reliably attached (no accidental reload).target_yis saved.popstate) — a genuine restore-path exercise without a second click that could race the event wiring.|scrollY − 500| ≤ 5) instead of a loose lower bound, so a wrong-but-plausible outcome (top, or clamped to the bottom) cannot pass.Verification
Stressed well beyond a single run:
14/14full CI-stylehatch test --headlessruns25/25runs under heavy CPU oversubscription (taskset -c 0,1 -n 2)15/15single-core pinned runshatch fmt(ruff + prettier) andpyrightcleanChecklist
Please update this checklist as you complete each item:
scroll_restorationis still unreleased)By submitting this pull request I agree that all contributions comply with this project's open source license(s).