Problem
settleUntil: 'networkidle' can resolve in the gap before a React startTransition-deferred server action dispatches its request, so for transition-driven UI it can settle on the pre-mutation state.
Real case from umami's run-an-event: the Status control is a controlled <select> whose onChange does startTransition(async () => { await updateStatus(); router.refresh() }). After selectOption, at the instant run() returns, React hasn't dispatched the action's fetch yet — so the page is momentarily "network-idle" and settleUntil:'networkidle' can resolve against the old value before the refresh repaints.
It happened to land correctly in the full render (the request was in flight by the time Playwright polled), but it's timing-dependent — and the preview PNG for that step showed the pre-settle transient ("Save the date" instead of "Tickets open"), which is how I noticed.
Why it matters
startTransition + Server Action + router.refresh() is the standard Next.js App Router mutation pattern, so this isn't an edge case — it's the common case for any "click a control, server updates, UI repaints" step. networkidle is the natural thing an author reaches for there, and it's subtly the wrong tool.
Suggested direction
Two parts:
- Docs: the new "Settling" section should explicitly steer transition-deferred mutations to a real
waitFor on committed UI state (e.g. expect(locator).toHaveValue(...) / wait for the badge to flip), not networkidle.
- Maybe a stronger settle: a
settleUntil variant that also waits for React to go idle (or that waits a tick for in-flight transitions to dispatch before checking network), so the obvious option is also the robust one.
Follow-up to #14.
Problem
settleUntil: 'networkidle'can resolve in the gap before a ReactstartTransition-deferred server action dispatches its request, so for transition-driven UI it can settle on the pre-mutation state.Real case from umami's
run-an-event: the Status control is a controlled<select>whoseonChangedoesstartTransition(async () => { await updateStatus(); router.refresh() }). AfterselectOption, at the instantrun()returns, React hasn't dispatched the action's fetch yet — so the page is momentarily "network-idle" andsettleUntil:'networkidle'can resolve against the old value before the refresh repaints.It happened to land correctly in the full render (the request was in flight by the time Playwright polled), but it's timing-dependent — and the
previewPNG for that step showed the pre-settle transient ("Save the date" instead of "Tickets open"), which is how I noticed.Why it matters
startTransition+ Server Action +router.refresh()is the standard Next.js App Router mutation pattern, so this isn't an edge case — it's the common case for any "click a control, server updates, UI repaints" step.networkidleis the natural thing an author reaches for there, and it's subtly the wrong tool.Suggested direction
Two parts:
waitForon committed UI state (e.g.expect(locator).toHaveValue(...)/ wait for the badge to flip), notnetworkidle.settleUntilvariant that also waits for React to go idle (or that waits a tick for in-flight transitions to dispatch before checking network), so the obvious option is also the robust one.Follow-up to #14.