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 (var snap: JsonValue?; ident: string) : JsonValue?
widget_click_point (var snap: JsonValue?; ident: string) : tuple<float;float>
widget_component_point (var snap: JsonValue?; ident: string; component: int) : tuple<float;float>
widget_payload_field (var snap: JsonValue?; ident: string; field: string) : JsonValue?
widget_rendered (var snap: JsonValue?; ident: string) : bool
- 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:
snap : JsonValue?
ident : string
- get_text(app: ImguiApp; uri: string ): string
GET uri; return the raw response body or “” (bounded by the per-request timeout).
- Arguments:
app : ImguiApp
uri : string
- 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:
snap : JsonValue?
ident : string
- 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:
snap : JsonValue?
ident : string
- 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:
snap : JsonValue?
ident : string
19.4.1.5. Actions
force_set_value (app: ImguiApp; target: string; var value: JsonValue?) : JsonValue?
force_set_verified (app: ImguiApp; target: string; value: auto(T); max_frames: int = 120) : auto
move_to (app: ImguiApp; pos: tuple<float;float>; duration_ms: int = 600)
reload (app: ImguiApp; timeout_sec: float = DEFAULT_RELOAD_TIMEOUT_SEC)
reset (app: ImguiApp; timeout_sec: float = DEFAULT_RELOAD_TIMEOUT_SEC)
reset_cursor_pos (pos: tuple<float;float> = tuple<tuple<float;float>>(0f,0f))
resize_window (app: ImguiApp; target: string; w: float; h: float) : JsonValue?
- click(app: ImguiApp; target: string ): JsonValue?
Synthesize a left-click on target (imgui_click verb).
- Arguments:
app : ImguiApp
target : string
- 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:
app : ImguiApp
target : string
- 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:
app : ImguiApp
target : string
- 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:
app : ImguiApp
target : string
- 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.
- 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:
app : ImguiApp
target : string
- 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:
app : ImguiApp
timeout_sec : float
- 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:
app : ImguiApp
timeout_sec : float
- 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_size → SetNextWindowSize). 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:
app : ImguiApp
target : string
- 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:
app : ImguiApp
target : string
- 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
close_button_voice (app: ImguiApp; dwell: uint; header_target: string; child_target: string)
combo_pick_voice (app: ImguiApp; dwell: uint; target: string; item: int)
hold_through_voice (app: ImguiApp; dwell: uint; clicks: array<string>)
menu_pick_voice (app: ImguiApp; dwell: uint; header_target: string; item_target: string)
record_check_rendered (app: ImguiApp; target: string; expected: bool; max_frames: int = 120) : bool
record_check_unchanged (app: ImguiApp; target: string; field: string; before: string) : bool
- 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>
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:
app : ImguiApp
ms : uint
- 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:
app : ImguiApp
- 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:
app : ImguiApp
frames : int
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:
app : ImguiApp
note : string
- snapshot(app: ImguiApp ): JsonValue?
Fetch the full widget snapshot (imgui_snapshot command) as a parsed JsonValue.
- Arguments:
app : ImguiApp
19.4.1.9. Polling / await
await_probe (app: ImguiApp; until: string = “quiescent”) : JsonValue?
wait_cond (app: ImguiApp; timeout_sec: float; pred: block<():void>) : bool
wait_for_key_idle (app: ImguiApp; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC) : bool
wait_for_mouse_idle (app: ImguiApp; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC) : bool
wait_until (app: ImguiApp; max_frames: int; pred: block<(var snap:JsonValue?):bool>) : JsonValue?
- 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:
app : ImguiApp
until : string
- 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:
app : ImguiApp
timeout_sec : float
- 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:
app : ImguiApp
timeout_sec : float
- 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:
app : ImguiApp
timeout_sec : float
- 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.
- wait_until_ready(app: ImguiApp; timeout_sec: float ): bool
Poll GET /status until 200 OK or timeout. Returns true on ready.
- Arguments:
app : ImguiApp
timeout_sec : float
- 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).
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:
app : ImguiApp
- unpause(app: ImguiApp )
POST /unpause to resume the host’s update().
- Arguments:
app : ImguiApp
- 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:
app : ImguiApp
body : block<void>
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.