19.4.1. Playwright — block-form test harness for daslang-live + dasImgui apps

Block-form test harness for daslang-live + dasImgui apps. Spawns a host process, configures a transport (HTTP by default), and runs a test block against the live registry — click a widget by path, set a value, await quiescence, snapshot the surface, expect a value or render state. The with_imgui_app(...) { ... } lifecycle handles launch / handshake / shutdown so each test stays a single block. The live-API port is derived from DEFAULT_LIVE_PORT + worker_index (dastest’s isolated-mode flag --worker-index N), so N parallel workers land on distinct ports and never collide.

Locators (find_widget, widget_exists, widget_payload_field, widget_rendered) resolve via the same path keys the boost registry uses; actions (click, type_text, force_set_value, drag, focus, open_widget, close_widget, reload) post to the live-command surface in imgui_boost_runtime and apply the matching AwaitModifiers. Polling helpers (wait_until, wait_for_payload_value, await_quiescent) wrap the timeout/retry loop so tests don’t reinvent it.

19.4.1.1. Constants

DEFAULT_LIVE_PORT = 9090

DEFAULT_LIVE_PORT:int const

DEFAULT_DEADLOCK_SANITY_SEC = 120f

DEFAULT_DEADLOCK_SANITY_SEC:float const

DEFAULT_READY_TIMEOUT_SEC = 30f

DEFAULT_READY_TIMEOUT_SEC:float const

MACOS_READY_TIMEOUT_SEC = 90f

MACOS_READY_TIMEOUT_SEC:float const

ATTACH_PROBE_SEC = 0.3f

ATTACH_PROBE_SEC:float const

DEFAULT_TEST_TIMEOUT_SEC = 120f

DEFAULT_TEST_TIMEOUT_SEC:float const

DEFAULT_RELOAD_TIMEOUT_SEC = 30f

DEFAULT_RELOAD_TIMEOUT_SEC:float const

DEFAULT_FRAME_WAIT_SEC = 10f

DEFAULT_FRAME_WAIT_SEC:float const

READY_POLL_INTERVAL_MS = 0x64

READY_POLL_INTERVAL_MS:uint const

READY_POLL_REQUEST_TIMEOUT_SEC = 2

READY_POLL_REQUEST_TIMEOUT_SEC:int const

FRAME_POLL_INTERVAL_MS = 0x64

FRAME_POLL_INTERVAL_MS:uint const

RECORD_FPS = 30

RECORD_FPS:int const

NARRATE_READ_MS = 0x1194

NARRATE_READ_MS:uint const

SETTLE_MS = 0x4b0

SETTLE_MS:uint const

CLICK_DWELL_MS = 0x5dc

CLICK_DWELL_MS:uint const

KEY_TAB = 512

KEY_TAB:int const

KEY_ENTER = 525

KEY_ENTER:int const

TYPE_LEAD_MIN_MS = 0x2bc

TYPE_LEAD_MIN_MS:uint const

TYPE_LEAD_MAX_MS = 0xbb8

TYPE_LEAD_MAX_MS:uint const

TYPE_PER_CHAR_S = 0.16f

TYPE_PER_CHAR_S:float const

TYPE_MIN_FIELD_S = 0.9f

TYPE_MIN_FIELD_S:float const

TYPE_FIELD_BEAT_MS = 0xb4

TYPE_FIELD_BEAT_MS:uint const

CLICK_RELEASE_MS = 130

CLICK_RELEASE_MS:int const

TYPE_EFFECT_FRAMES = 120

TYPE_EFFECT_FRAMES:int const

COMBO_LEAD_MIN_MS = 0x2bc

COMBO_LEAD_MIN_MS:uint const

COMBO_LEAD_MAX_MS = 0xbb8

COMBO_LEAD_MAX_MS:uint const

COMBO_OPEN_INSET = 40f

COMBO_OPEN_INSET:float const

COMBO_TRAVEL_MS = 450

COMBO_TRAVEL_MS:int const

COMBO_POLL_FRAMES = 120

COMBO_POLL_FRAMES:int const

TREE_LEAD_MIN_MS = 0x2bc

TREE_LEAD_MIN_MS:uint const

TREE_LEAD_MAX_MS = 0xbb8

TREE_LEAD_MAX_MS:uint const

TREE_ARROW_FRAC = 0.6f

TREE_ARROW_FRAC:float const

TREE_TRAVEL_MS = 450

TREE_TRAVEL_MS:int const

TREE_POLL_FRAMES = 120

TREE_POLL_FRAMES:int const

CLOSE_BTN_FRAC = 0.5f

CLOSE_BTN_FRAC:float const

RECORD_ASSET_REL = "doc/source/_static/tutorials"

RECORD_ASSET_REL:string const

19.4.1.2. Structures

ImguiApp

struct ImguiApp

VoEntry
Fields:
  • hash : string - < say_hash(spoken) — links a narration line to its wav

  • text : string - < on-screen caption (the say() text arg)

  • voice : string - < spoken line fed to TTS (say() voice arg, or text if none)

  • wav : string - < wav basename under the voiceover/ dir

  • dur_s : float - < measured wav duration, seconds

VoManifest

struct VoManifest

VoSidecarHit
Fields:
  • frame : int - < recorded APNG frame index at the caption’s appearance — the sync anchor

  • t_s : float - < wall-clock seconds from record_start (reference/debug; convert uses frame)

  • wav : string - < wav basename to mux

  • text : string - < narration line (for reference)

VoSidecar

struct VoSidecar

19.4.1.3. App lifecycle

19.4.1.3.1. with_imgui_app

with_imgui_app(feature_path: string; body: block<(app:ImguiApp):void> )

Spawn daslang-live <feature_path> with dasImgui loaded; run body while alive, then POST /shutdown and drain stdout. Panics on non-zero exit / ready-timeout / test-timeout. The common case (require chain needs ONLY dasImgui); delegates to the 5-arg overload.

Arguments:
  • feature_path : string

  • body : block<(app: ImguiApp):void>

with_imgui_app(feature_path: string; extra_module_roots: array<string>; feature_root: string; feature_prefix: string; body: block<(app:ImguiApp):void> )

19.4.1.3.2. with_recording_app

with_recording_app(feature_path: string; output_apng_basename: string; max_seconds: int; body: block<(app:ImguiApp):void> )

Native-fps form (the common case). Delegates to the 4-arg overload — gen2’s trailing-block sugar can’t see through a default parameter between the last positional and the block, so the overload pair keeps the common call site clean.

Arguments:
  • feature_path : string

  • output_apng_basename : string

  • max_seconds : int

  • body : block<(app: ImguiApp):void>

with_recording_app(feature_path: string; output_apng_basename: string; max_seconds: int; fps: int; body: block<(app:ImguiApp):void> )
with_recording_app(feature_path: string; output_apng_basename: string; max_seconds: int; fps: int; extra_module_roots: array<string>; feature_root: string; asset_root: string; body: block<(app:ImguiApp):void> )

19.4.1.4. Locators / queries

find_widget(snap: JsonValue?; ident: string ): JsonValue?

Navigate to snapshot.globals[ident]; may return a JV(null) placeholder for missing keys (daslib/json ?[] quirk). For an existence check use widget_exists.

Arguments:
get_text(app: ImguiApp; uri: string ): string

GET uri; return the raw response body or “” (bounded by the per-request timeout).

Arguments:
widget_click_point(snap: JsonValue?; ident: string ): tuple<float;float>

The point imgui_click aims at for ident: the left glyph for the toggle family (checkbox / radio_button / radio_button_int), the bbox center otherwise. Mirrors resolve_target_bbox in imgui_boost_runtime — keep the kind list in sync with that resolver.

Arguments:
widget_component_point(snap: JsonValue?; ident: string; component: int ): tuple<float;float>

Center of multi-component widget ident’s sub-rect component (slider/drag _2/3/4, drag_*_range2), read from sub_bboxes telemetry — the deterministic way to target one handle of a vector/range widget. Falls back to widget_click_point when sub_bboxes is absent.

Arguments:
  • snap : JsonValue?

  • ident : string

  • component : int

widget_exists(snap: JsonValue?; ident: string ): bool

True iff globals[ident] is a real widget entry — works around the daslib/json ?[] quirk by checking the always-present kind field.

Arguments:
widget_payload_field(snap: JsonValue?; ident: string; field: string ): JsonValue?

Navigate to snapshot.globals[ident].payload[field]; returns null if any link is missing.

Arguments:
  • snap : JsonValue?

  • ident : string

  • field : string

widget_rendered(snap: JsonValue?; ident: string ): bool

True iff globals[ident] EXISTS and painted this frame. Unpainted entries carry "rendered": false; painted widgets omit it (?? true). The leading existence check is load-bearing: an absent/misspelled path yields null ?? true — a false pass — so it must report not-rendered.

Arguments:

19.4.1.5. Actions

click(app: ImguiApp; target: string ): JsonValue?

Synthesize a left-click on target (imgui_click verb).

Arguments:
click_at(events: array<JsonValue?>; t_ms: int; pos: tuple<float;float>; travel_ms: int = 500; button: int = 0 )

Append lerp-from-last + press + release + dwell to events: cursor travels from the tracked previous position to pos over travel_ms, then button button is pressed and released. Updates the tracked position.

Arguments:
  • events : array< JsonValue?>

  • t_ms : int

  • pos : tuple<float;float>

  • travel_ms : int

  • button : int

close_widget(app: ImguiApp; target: string ): JsonValue?

Flip target.state.open = false (imgui_close verb) — closes a window/tree node from the test driver.

Arguments:
double_click(app: ImguiApp; target: string ): JsonValue?

Faithful double-click on target’s bbox center: two press/release taps so IsMouseDoubleClicked fires. Frame-paced (t_ms = frame index): the host advances one event per frame, so the two taps land on distinct frames — never collapsed into one the way a wall-clock timeline is on a slow host (which drops the double-click).

Arguments:
drag(app: ImguiApp; target: string; dx: float; dy: float; steps: int = 4; button: int = 0 ): JsonValue?

Synthesize a mouse-drag on target’s bbox center: warp → press → drag to center+(dx,dy) → release, as an imgui_mouse_play timeline (bbox resolved client-side). Returns {ok, value} (ok=false if the target is absent/unrendered — else it would drag from a defaulted (0,0) bbox). steps scales duration.

Arguments:
  • app : ImguiApp

  • target : string

  • dx : float

  • dy : float

  • steps : int

  • button : int

drag_along(events: array<JsonValue?>; t_ms: int; from_pos: tuple<float;float>; to_pos: tuple<float;float>; drag_ms: int; approach_ms: int = 400; button: int = 0 )

Append lerp-to-start + press + drag + release + dwell to events: cursor travels from the tracked position to from_pos over approach_ms, then presses and drags to to_pos over drag_ms. Updates the tracked position to to_pos.

Arguments:
  • events : array< JsonValue?>

  • t_ms : int

  • from_pos : tuple<float;float>

  • to_pos : tuple<float;float>

  • drag_ms : int

  • approach_ms : int

  • button : int

drag_to(app: ImguiApp; source: string; target: string; steps: int = 6; button: int = 0 ): JsonValue?

Drag from source widget center to target widget center. Reads both bboxes from a fresh snapshot, computes the delta, and dispatches the L1 drag coroutine (press at source → interpolated moves → release).

Arguments:
  • app : ImguiApp

  • source : string

  • target : string

  • steps : int

  • button : int

focus(app: ImguiApp; target: string ): JsonValue?

Force keyboard focus to the next render of target (imgui_focus verb); display-only widgets reply Result.err("not focusable").

Arguments:
force_set_value(app: ImguiApp; target: string; value: JsonValue? ): JsonValue?

Set target’s payload value remotely (imgui_force_set verb); value is the JSON form expected by the widget kind.

Arguments:
force_set_verified(app: ImguiApp; target: string; value: auto(T); max_frames: int = 120 ): auto

Recording-as-test force_set: set target.value to the typed value and verify it took, accumulating a miss (surfaced at with_recording_app teardown) rather than panicking mid-body. Use only inside a with_recording_app body — raw tests want the panicking expect_value.

Arguments:
  • app : ImguiApp

  • target : string

  • value : auto(T)

  • max_frames : int

key_tap(app: ImguiApp; key: int; hold_frames: int = 2 )

Synthesize a discrete press+release of an ImGuiKey by integer code, FRAME-PACED: held for hold_frames frames then released via imgui_key_play, so it fires exactly once regardless of frame rate (a wall-clock hold crosses ImGui’s key-repeat threshold on a slow frame and over-fires). Use int(ImGuiKey.Tab) etc.

Arguments:
  • app : ImguiApp

  • key : int

  • hold_frames : int

move_to(app: ImguiApp; pos: tuple<float;float>; duration_ms: int = 600 )

Post a lerped cursor move from the last tracked position to pos (updates the tracked position). duration_ms is a FLOOR: actual duration scales with distance (≈0.325 px/ms, constant cursor speed) so a long move doesn’t cover its span as fast as a short one and read as a teleport.

Arguments:
  • app : ImguiApp

  • pos : tuple<float;float>

  • duration_ms : int

open_widget(app: ImguiApp; target: string ): JsonValue?

Flip target.state.open = true (imgui_open verb) — reopens a window/tree node from the test driver.

Arguments:
reload(app: ImguiApp; timeout_sec: float = DEFAULT_RELOAD_TIMEOUT_SEC )

POST /reload then poll /status until has_error == false; panics on timeout. Stays on raw HTTP — reload is daslang-live process control, not a dasImgui command verb.

Arguments:
reset(app: ImguiApp; timeout_sec: float = DEFAULT_RELOAD_TIMEOUT_SEC )

POST /reset — re-simulate the already-compiled program into a fresh context (no parse/infer), clearing @live state. ~100x cheaper than reload; use between subtests instead of respawning. Polls /status until generation advances; panics if no baseline generation, has_error, or timeout.

Arguments:
reset_cursor_pos(pos: tuple<float;float> = tuple<tuple<float;float>>(0f,0f) )

Reset the tracked previous cursor position. Call between recordings if the host’s synth cursor state was already non-(0,0).

Arguments:
  • pos : tuple<float;float>

resize_window(app: ImguiApp; target: string; w: float; h: float ): JsonValue?

Resize the window named by target to (w, h) on the next frame, via imgui_set_window_size (feeds state.pending_sizeSetNextWindowSize). Named resize_window (not set_window_size) to avoid the overload collision with imgui_containers_builtin’s in-frame form.

Arguments:
  • app : ImguiApp

  • target : string

  • w : float

  • h : float

right_click(app: ImguiApp; target: string ): JsonValue?

Synthesize a right-click on target (imgui_click verb with button = 1). Used by popup_context_item / popup_context_window drivers.

Arguments:
select_widget(app: ImguiApp; target: string ): JsonValue?

Activate target tab_item (imgui_select verb) — the automation counterpart of clicking a tab (ImGui owns active-tab selection, so an inactive tab can’t be reached by click).

Arguments:
type_text(app: ImguiApp; target: string; text: string; focus_max_frames: int = 120; duration_s: float = 0f ): JsonValue?

Auto-focus target, wait up to focus_max_frames for focus, then stream text as synthetic key+char events via imgui_key_type (into whichever widget holds focus). Returns {ok, value} (ok=false on empty text or if focus never settles). duration_s > 0 paces typing over wall-clock.

Arguments:
  • app : ImguiApp

  • target : string

  • text : string

  • focus_max_frames : int

  • duration_s : float

19.4.1.6. Recording / narration

click_render_voice(app: ImguiApp; dwell: uint; click_target: string; watch_target: string; expect_rendered: bool )

Real-click click_target under a voice line, then assert watch_target’s rendered state == expect_rendered — the reveal (true) / dismiss (false) rail. A button whose own click closes the popup can’t be verified by click-count (gone next frame); the watch-target’s render state is the durable signal.

Arguments:
  • app : ImguiApp

  • dwell : uint

  • click_target : string

  • watch_target : string

  • expect_rendered : bool

close_button_voice(app: ImguiApp; dwell: uint; header_target: string; child_target: string )

Click the X-button of a CLOSABLE container header under a voice line, then assert the strip vanished (child_target no longer rendered). The X sits a half header-height in from the header’s right edge (mirrors ImGui’s layout). Self-verifying at teardown; caller must move_to the header first.

Arguments:
  • app : ImguiApp

  • dwell : uint

  • header_target : string

  • child_target : string

combo_pick_voice(app: ImguiApp; dwell: uint; target: string; item: int )

Open combo target and click item item from its popup under a voice line, then assert target.value == item. Clicks the box (inset from the left, off the label), POLLS until the popup renders per-item sub_bboxes, then clicks item item. Self-verifying at teardown.

Arguments:
  • app : ImguiApp

  • dwell : uint

  • target : string

  • item : int

drag_through_voice(app: ImguiApp; dwell: uint; target: string; to_pos: tuple<float;float>; drag_ms: int = 0; field: string = "value" )

Drag analog of hold_through_voice: scrub from the tracked cursor to to_pos under a voice line, then assert the drag CHANGED target.payload[field] (a no-op drag aborts at teardown). The caller must move_to the handle first. field defaults to "value"; docking passes "size"/"dock_id".

Arguments:
  • app : ImguiApp

  • dwell : uint

  • target : string

  • to_pos : tuple<float;float>

  • drag_ms : int

  • field : string

hold_through_voice(app: ImguiApp; dwell: uint; clicks: array<string> )

Pace the click(s) in clicks UNDER an already-posted voice line: a short lead, then each click lands (verified, or the recording aborts), then hold the caption for the rest. dwell is say_begin’s return; the caller must have moved the cursor onto the first target.

Arguments:
  • app : ImguiApp

  • dwell : uint

  • clicks : array<string>

menu_pick_voice(app: ImguiApp; dwell: uint; header_target: string; item_target: string )

Open a menu-bar menu (header_target) and pick item_target under a voice line, navigating like a human: click the header, then drop STRAIGHT DOWN the column. A diagonal click would cross a SIBLING header mid-travel and ImGui hover-switches the open menu, landing the click in the wrong one. Self-verifying at teardown.

Arguments:
  • app : ImguiApp

  • dwell : uint

  • header_target : string

  • item_target : string

record_check(app: ImguiApp; desc: string; ok: bool ): bool

Recording-safe verifier for a condition the CALLER already evaluated — the escape hatch for an effect the field-based record_check_* can’t reach (a value nested in a payload ARRAY, a cross-widget invariant). Pass the bool you computed; on false it accumulates desc into the failure ledger. Returns ok.

Arguments:
  • app : ImguiApp

  • desc : string

  • ok : bool

record_check_changed(app: ImguiApp; target: string; field: string; before: string; max_frames: int = 120 ): bool

Recording-safe “the gesture moved it” verifier: assert target.payload[field]’s JSON now DIFFERS from before within max_frames — the honest self-test for a continuous drag (a human drag lands on no exact pixel, so assert CHANGED not equals). Accumulate-on-miss; returns true once it differs.

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • before : string

  • max_frames : int

record_check_kind_count(app: ImguiApp; kind_prefix: string; min_count: int; max_frames: int = 120 ): bool

Recording-safe “did the rail paint” verifier for entries under SYNTHESIZED path keys (drawlist primitives, kind "add_*"): assert the registry holds ≥ min_count entries whose kind starts with kind_prefix — the only honest signal when the call sites have no stable ident. Accumulate-on-miss.

Arguments:
  • app : ImguiApp

  • kind_prefix : string

  • min_count : int

  • max_frames : int

record_check_rendered(app: ImguiApp; target: string; expected: bool; max_frames: int = 120 ): bool

Recording-safe “did the gesture show/hide it” verifier: assert target’s rendered-this-frame state (widget_rendered) equals expected — the robust open/close signal for a container (collapsing a tree flips its child to rendered:false), readable even when opened/flags are absent. Accumulate-on-miss.

Arguments:
  • app : ImguiApp

  • target : string

  • expected : bool

  • max_frames : int

record_check_unchanged(app: ImguiApp; target: string; field: string; before: string ): bool

Recording-safe “the gesture left it alone” verifier — mirror of record_check_changed: assert target.payload[field] still EQUALS before, the honest self-test for a NEGATIVE claim. No event to wait for, so the caller must settle first (wait_for_mouse_idle); compares once. Accumulate-on-miss.

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • before : string

record_check_value(app: ImguiApp; target: string; field: string; expected: auto(T); max_frames: int = 120 ): bool

Recording-safe verifier: assert target.payload[field] == expected within max_frames, accumulating a miss instead of panicking (a mid-body panic would skip record_stop + /shutdown and orphan the host). The public rail for self-verifying a NON-CLICK effect; returns true on match.

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • expected : auto(T)

  • max_frames : int

19.4.1.6.1. say

say(app: ImguiApp; text: string; target: string = ""; read_ms: uint = NARRATE_READ_MS; voice: string = "" )

Pop a narration overlay (imgui_narrate verb), then hold the dwell so the recorder captures the window. Named say (not narrate) to avoid overload churn with imgui_visual_aids::narrate. For a click/drag that must land under the voice, use say_begin + a manual dwell split.

Arguments:
  • app : ImguiApp

  • text : string

  • target : string

  • read_ms : uint

  • voice : string

say(app: ImguiApp; text: string; targets: array<string>; read_ms: uint = NARRATE_READ_MS; voice: string = "" )

19.4.1.6.2. say_begin

say_begin(app: ImguiApp; text: string; target: string = ""; read_ms: uint = NARRATE_READ_MS; voice: string = "" ): uint

Non-blocking say: post the narration overlay (+ record the voiceover anchor) and RETURN the dwell in ms WITHOUT holding — the caller splits it around a click/drag so the action lands under the voice. text is the caption; voice (when set) is the spoken line; use say to post+hold in one call.

Arguments:
  • app : ImguiApp

  • text : string

  • target : string

  • read_ms : uint

  • voice : string

say_begin(app: ImguiApp; text: string; targets: array<string>; read_ms: uint = NARRATE_READ_MS; voice: string = "" ): uint

say_hash(text: string ): string

Stable wav key shared by prepare_recording.das and say(); hash() is deterministic.

Arguments:
  • text : string

toggle_tree_voice(app: ImguiApp; dwell: uint; header_target: string; on_arrow: bool; child_target: string; expect_child_rendered: bool )

Click a tree_node/collapsing_header header under a voice line, then assert the toggle via child_target’s rendered state (a collapsed node stops drawing children). on_arrow=true clicks the chevron, on_arrow=false the label — the OpenOnArrow distinction (arrow toggles, label no-op). Self-verifying at teardown.

Arguments:
  • app : ImguiApp

  • dwell : uint

  • header_target : string

  • on_arrow : bool

  • child_target : string

  • expect_child_rendered : bool

type_into_voice(app: ImguiApp; dwell: uint; target: string; click_pos: tuple<float;float>; cells: array<string>; expected: auto(T) ): auto

Type analog of drag_through_voice: REAL keyboard edit of target, assert committed == expected. CLICKS click_pos (not imgui_focus) because InputScalar’s AutoSelectAll fires only on activation, so the click focuses AND selects → typing REPLACES. Per cells: type, Tab between, Enter; caller must move_to click_pos first.

Arguments:
  • app : ImguiApp

  • dwell : uint

  • target : string

  • click_pos : tuple<float;float>

  • cells : array<string>

  • expected : auto(T)

19.4.1.7. Content-time gating

hold_content(app: ImguiApp; ms: uint )

Hold for ms of CONTENT time: under lockstep recording wait for ms/1000*fps captured frames; otherwise a plain wall-clock sleep(ms).

Arguments:
hold_remainder_content(app: ImguiApp; dwell_ms: uint; start_frames: int )

Pad a beat to dwell_ms of CONTENT time: wait until the recorder reaches start_frames + dwell_ms/1000*fps (start_frames from record_frame_count right after say_begin). Work done mid-dwell consumes content frames; this holds the rest. No-op when idle.

Arguments:
  • app : ImguiApp

  • dwell_ms : uint

  • start_frames : int

record_frame_count(app: ImguiApp ): int

Current captured-frame count (content-time index). Capture right after say_begin to anchor a hold_remainder_content. Returns 0 outside a recording session.

Arguments:
wait_content_frames(app: ImguiApp; frames: int )

Block until the recorder captures frames more frames (= frames/fps s of video); no-op outside a recording session. Anchored on the first VALID status read (a null keeps polling, never ends early). If capture stalls for RECORD_STALL_US, degrade to a wall-clock sleep so the beat lands and CI never hangs.

Arguments:

19.4.1.8. Snapshots

expect_render(app: ImguiApp; widget_ident: string; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC )

Assertion form of wait_for_render; panics on timeout with the list of currently-registered widget identifiers (so typos in widget_ident surface).

Arguments:
  • app : ImguiApp

  • widget_ident : string

  • timeout_sec : float

expect_value(app: ImguiApp; target: string; field: string; expected: auto(T); max_frames: int = 300 )

Assertion form of wait_for_payload_value; panics with a focused want/got/kind/rendered message on timeout.

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • expected : auto(T)

  • max_frames : int

log_snapshot(app: ImguiApp; note: string = "" )

Dump the live UI state to the log (LOG_WARNING) — per registered widget: ident, kind, rendered flag, bbox, and payload.value. The way to see what a headless/CI run actually painted; pair it with a failed assertion so a red run is self-explaining.

Arguments:
snapshot(app: ImguiApp ): JsonValue?

Fetch the full widget snapshot (imgui_snapshot command) as a parsed JsonValue.

Arguments:

19.4.1.9. Polling / await

auto_dump_on_timeout_enabled(): bool

def auto_dump_on_timeout_enabled () : bool

await_probe(app: ImguiApp; until: string = "quiescent" ): JsonValue?

Single imgui_await POST; the AwaitResult lives under response.value. For poll-until-quiescent use await_quiescent.

Arguments:
await_quiescent(app: ImguiApp; timeout_sec: float = 5f )

Assertion wrapper over await_imgui(transport, "quiescent", timeout); panics on timeout. For a non-asserting poll, call await_imgui directly.

Arguments:
set_auto_dump_on_timeout(enabled: bool )

Toggle the wait-family auto-dump (default on): any wait_* that gives up logs the UI state once via log_snapshot, so a red run is self-explaining. Turn it off around a negative wait (the timeout is the expected outcome) so the log isn’t spammed with an expected miss.

Arguments:
  • enabled : bool

wait_cond(app: ImguiApp; timeout_sec: float; pred: block<():void> ): bool

Bare wall-clock condition poll — no snapshot, no widget coupling. Polls pred() until true, up to timeout_sec real seconds; returns true on match, false on timeout. Use for state gated on a command round-trip; the frame-count busy-poll it replaces expires prematurely on the multi-kHz headless server.

Arguments:
  • app : ImguiApp

  • timeout_sec : float

  • pred : block<void>

wait_for_bool_value(app: ImguiApp; target: string; field: string; expected: bool; max_frames: int = 300 ): bool

Poll snapshot until globals[target].payload[field] == expected (typed bool).

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • expected : bool

  • max_frames : int

wait_for_hover(app: ImguiApp; widget_ident: string; expected: bool = true; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC ): JsonValue?

Poll until widget widget_ident’s hover (IsItemHovered) equals expected. Sync gate for synthetic clicks: without it a batched move``+``press fires before HoveredId is set (the press-before-hover race real mice never hit). The widget_exists guard stops an absent ident false-passing expected=false.

Arguments:
  • app : ImguiApp

  • widget_ident : string

  • expected : bool

  • timeout_sec : float

wait_for_int_value(app: ImguiApp; target: string; field: string; expected: int; max_frames: int = 300 ): bool

Poll snapshot until globals[target].payload[field] == expected (typed int). Returns true on match, false after max_frames frames elapse.

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • expected : int

  • max_frames : int

wait_for_key_idle(app: ImguiApp; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC ): bool

Poll imgui_key_status until playing == false or timeout; returns true on idle. Uses one HTTP request per poll (unlike a wait_until body that pairs a snapshot with a status call), keeping the per-subprocess connection count low on Windows.

Arguments:
wait_for_mouse_idle(app: ImguiApp; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC ): bool

Poll imgui_mouse_status until playing == false or timeout; returns true on idle. Uses one HTTP request per poll (unlike a wait_until body that pairs a snapshot with a status call), keeping the per-subprocess connection count low on Windows.

Arguments:
wait_for_payload_value(app: ImguiApp; target: string; field: string; expected: auto(T); max_frames: int = 300 ): bool

Generic poll: snapshot until globals[target].payload[field] == expected; T is inferred from expected. Use for any roundtrippable type (float2/3/4, int2/3/4, etc.) where the typed sugars above don’t apply.

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • expected : auto(T)

  • max_frames : int

wait_for_render(app: ImguiApp; widget_ident: string; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC ): JsonValue?

First-paint gate — alias of wait_for_visible(). widget_rendered now requires existence, so this no longer false-passes for an absent path. Prefer wait_for_visible at call sites that target a widget which can hide and re-show.

Arguments:
  • app : ImguiApp

  • widget_ident : string

  • timeout_sec : float

wait_for_string_value(app: ImguiApp; target: string; field: string; expected: string; max_frames: int = 300 ): bool

Poll snapshot until globals[target].payload[field] == expected (typed string).

Arguments:
  • app : ImguiApp

  • target : string

  • field : string

  • expected : string

  • max_frames : int

wait_for_visible(app: ImguiApp; widget_ident: string; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC ): JsonValue?

Poll snapshot until widget_ident is registered AND painted this frame (widget_rendered). The robust gate for a widget that can hide and re-show: wait_for_widget returns on a stale registry entry (re-opened popup/menu) while it’s off-screen, so a click fired right after can be silently dropped.

Arguments:
  • app : ImguiApp

  • widget_ident : string

  • timeout_sec : float

wait_for_widget(app: ImguiApp; widget_ident: string; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC ): JsonValue?

Poll snapshot until globals[widget_ident] is present (registered at module init OR rendered at least once). Use for subprocess-startup gating; for first-paint semantics use wait_for_render.

Arguments:
  • app : ImguiApp

  • widget_ident : string

  • timeout_sec : float

wait_until(app: ImguiApp; max_frames: int; pred: block<(var snap:JsonValue?):bool> ): JsonValue?

Poll pred(snap) up to max_frames 60fps-equivalent server frames (max_frames/60 s); returns the matching snapshot or null on timeout. max_frames is translated to wall-clock at 60fps (not a raw frame-counter delta, which expires in ~ms on the multi-kHz headless server); prefer wait_until_sec for new code.

Arguments:
wait_until_ready(app: ImguiApp; timeout_sec: float ): bool

Poll GET /status until 200 OK or timeout. Returns true on ready.

Arguments:
wait_until_sec(app: ImguiApp; timeout_sec: float; pred: block<(var snap:JsonValue?):bool> ): JsonValue?

Wall-clock variant of wait_until: poll pred(snap) until true, up to timeout_sec real seconds; returns the matching snapshot or null on timeout. Prefer over wait_until for time-driven server work — the frame-budget form fires prematurely in headless (multi-kHz, no vsync).

Arguments:

19.4.1.10. Pause control

pause(app: ImguiApp )

POST /pause to halt the host’s update() (skips advance_coroutines + render). POST /command still dispatches and POSTs still drain via server.tick(). Useful for deterministic observation: probe imgui_await while the world is frozen, then unpause.

Arguments:
unpause(app: ImguiApp )

POST /unpause to resume the host’s update().

Arguments:
with_paused(app: ImguiApp; body: block<():void> )

Scope: pause the host, invoke body, unpause. Body observes a frozen world — coroutines spawned by imgui_click sit unadvanced until unpause, so imgui_await reports pending_coroutines >= 1 deterministically even in headless mode (where the advance races the next POST).

Arguments:

19.4.1.11. Transport plumbing

post_command(app: ImguiApp; name: string; args: JsonValue? ): JsonValue?

Send {"name":<name>,"args":<args>} through app.transport; returns parsed JSON or null on transport failure. Escape hatch for custom verbs outside the shipped helper set.

Arguments: