search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

Frame production

Every dusk read and every dusk action depends on the app producing frames. When it stops, both go wrong at once and neither looks like a tooling problem. This page describes when that happens, what dusk does about it, and the one call that fixes it.

When frame production stops

Flutter turns frame production off for three lifecycle states:

AppLifecycleState Frames
resumed, inactive on
hidden, paused, detached off

On Flutter web, Chrome reports document.visibilityState: "hidden" as soon as the tab is backgrounded or its window falls behind another, and the engine maps that to AppLifecycleState.hidden. Starting the app detached is enough to land there without touching anything.

What breaks, and why it reads as a product defect

Two separate symptoms, one cause:

  • A snapshot loses its text. Semantics labels are rebuilt during a frame. With no frames, dusk:snap returns the buttons and none of the - text nodes, so a screen that renders perfectly reads as completely empty. This has been mistaken for "the dashboard is permanently stuck on loading skeletons"; the screen was fine and a screenshot proved it.
  • An action reports success and changes nothing. A gesture is dispatched into the widget tree, but the frame that would apply it never runs. The handler returns a clean payload for a tap that could not possibly land.

What dusk does

Bounded settling

Handlers settle a gesture or an edit by awaiting one or two frames. Those awaits go through awaitFrameOrTimeout / awaitFramesOrTimeout and fall through after kFrameSyncTimeout (200ms per frame) rather than blocking on WidgetsBinding.instance.endOfFrame, which never completes while frames are off. Without the bound a dusk:tap against a hidden page sat for 45s+ with no output and no error until the caller's own timeout killed it.

A healthy engine is unaffected: a real frame lands in ~16ms and always wins the race.

The warnings block

Every ext.dusk.* success payload gains a warnings key while frame production is off:

{
  "snapshot": "...",
  "groupId": "snapshot-1700000000000",
  "warnings": {
    "framesEnabled": false,
    "lifecycleState": "hidden",
    "hint": "The engine is not producing frames, so the semantics tree is not being rebuilt and dispatched gestures cannot take effect. A backgrounded browser tab is the usual cause. Bring the page to front (CDP Page.bringToFront) and retry before trusting this result."
  }
}
Field Type Description
framesEnabled bool Always false when the block is present.
lifecycleState string | null The AppLifecycleState behind it (hidden, paused, detached), or null before the first lifecycle message.
hint string Fixed advisory naming the usual cause and the fix.

The block is omitted entirely on a healthy engine, so its presence is itself the signal and a clean run carries no extra bytes.

CLI banner. Commands that summarise rather than print the raw envelope (dusk:snap prints only the tree, dusk:tap prints ✓ Tapped e7) would drop the block, so they print a banner to stderr instead. stdout stays the payload:

⚠ The app is not producing frames (lifecycle: hidden). The semantics tree is not being
  rebuilt and gestures cannot take effect, so this result may be stale. A backgrounded
  browser tab is the usual cause: bring the page to front and retry.

The fix

Bring the page to front over CDP before interacting:

# The app must have been started with a CDP port for this to be reachable.
./bin/fsa start --device=chrome --cdp-port=9333
{ "method": "Page.bringToFront" }

One call flips the page back to visible and focused: the text nodes return, and a tap that hung answers immediately. When dusk behaves strangely on web, read document.hidden before concluding anything about the app.

Two blank-screen causes look identical and are not, so read the console entry count before deciding which one you have. A mounted tree with missing text is a hidden page and Page.bringToFront fixes it. Zero glass panes, zero canvases and zero console lines is a dead bootstrap instead, which no amount of bringing-to-front repairs; restart the run.