Skip to content

Wait for idle sync before capturing screenshot#86

Draft
EmilioBejasa wants to merge 4 commits intomainfrom
fix/screenshot-timing
Draft

Wait for idle sync before capturing screenshot#86
EmilioBejasa wants to merge 4 commits intomainfrom
fix/screenshot-timing

Conversation

@EmilioBejasa
Copy link
Copy Markdown
Collaborator

Summary

After JS calls notifyStoryReady() (at the end of a useEffect), Fabric may still have pending layout and mount work queued on the main thread. Taking the screenshot immediately after the latch releases can capture a partially-rendered frame, causing flaky diffs in Screenshotbot.

waitForIdleSync() drains the main thread's Looper before Screenshot.snap() runs, ensuring all native view updates from the Fabric commit are applied first.

The race condition

JS useEffect fires → notifyStoryReady() → latch releases
                                              ↓
                              [main thread still processing Fabric mount]
                                              ↓
                              Screenshot.snap() ← taken too early ⚠️

After this fix:

JS useEffect fires → notifyStoryReady() → latch releases → waitForIdleSync()
                                                                    ↓
                                              [main thread drains]
                                                                    ↓
                                              Screenshot.snap() ← all views painted ✓

🤖 Generated with Claude Code

EmilioBejasa and others added 2 commits March 18, 2026 15:58
After JS calls notifyStoryReady() (end of useEffect), Fabric may still
have pending layout and mount work queued on the main thread. Taking the
screenshot immediately could capture a partially-rendered frame.

waitForIdleSync() drains the main thread's Looper before snap() runs,
ensuring all native view updates are applied first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fabric posts a second round of layout/mount work (e.g. Switch thumb
animation, text measurement) in response to the first commit, which
races with a single waitForIdleSync(). A second call catches that.

Also make awaitStoryReady() throw on timeout instead of silently
proceeding, so a story that never calls notifyStoryReady() fails
loudly rather than capturing a partial screenshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@EmilioBejasa EmilioBejasa marked this pull request as draft March 19, 2026 16:38
EmilioBejasa and others added 2 commits March 19, 2026 13:01
2 rounds was still not enough for SwitchCompat: mount → setChecked →
animation setup → layout measurement spans several async dispatch cycles
in Fabric. 5 rounds covers the full chain empirically.

Made the count overridable via getIdleSyncRounds() for consumers who
need to tune it for their own native components.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
waitForIdleSync() only drains the Looper. Fabric's mount phase is
scheduled as a ReactChoreographer FrameCallback (VSYNC-driven), so
it was invisible to waitForIdleSync() — the screenshot was taken
before native views were actually updated.

Fix 1: replace waitForIdleSync() loops with awaitChoreographerFrames(),
which posts a FrameCallback and blocks until it fires, then drains the
Looper for any follow-up Looper work in each frame.

Fix 2: disable system animation scales via UiAutomation before the test
run so SwitchCompat's thumb animation (and similar) can't render in an
intermediate state at snapshot time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant