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.
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
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
4.1.3. App lifecycle
4.1.3.1. with_imgui_app
- with_imgui_app(feature_path: string; body: block<(app:ImguiApp):void> )
Spawn daslang-live <feature_path> with the dasImgui module loaded; runs
body while alive, then POSTs /shutdown and drains stdout. Panics on
non-zero exit code, ready-timeout, or test-timeout so the surrounding
[test] fails. The common case — for a feature whose require chain
needs ONLY dasImgui. Delegates to the 5-arg overload with no extra modules.
- 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> )
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 — daslang’s gen2 trailing-block sugar can’t see through a default parameter sitting 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> )
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 (square check / radio
circle) for the toggle family — checkbox / radio_button / radio_button_int — and the bbox
center otherwise. Mirrors resolve_target_bbox in imgui_boost_runtime so a recording’s
move_to lands exactly where the click fires (no mid-row → glyph hop). 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_float2/3/4,
slider_int2/3/4, drag_float2/3/4, drag_int2/3/4, drag_*_range2), read from the sub_bboxes
telemetry — the deterministic way to target a single handle of a vector/range widget, instead
of guessing an x within the combined bbox. Falls back
to widget_click_point when sub_bboxes is absent (single-component widget, component out
of range, or a host predating the field).
- 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. Registered-but-unpainted entries
carry "rendered": false; painted widgets omit the field (?? true). The leading
existence check is load-bearing: an absent (or misspelled) path yields null ?? true — a
false pass — so a never-registered ident must report not-rendered, not silently “rendered”.
- Arguments:
snap : JsonValue?
ident : string
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 mouse 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 — e.g. enter-directory / open-file in a file dialog.
Re-snapshots to resolve the center client-side (like drag); the target must be rendered.
Frame-paced (t_ms = frame index, like drag): the host advances exactly one event
per frame, so the two taps are guaranteed to land on distinct frames a couple of frames
apart — well inside MouseDoubleClickTime at any frame rate, and never collapsed into one
frame the way a wall-clock timeline can be on a slow/stalled host (which drops the double-click).
Returns {ok=false, error} when target isn’t in the snapshot or isn’t rendered this
frame (widget_click_point would otherwise default the bbox to (0,0) and click nowhere).
- 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, composed as an imgui_mouse_play timeline. Resolves
the bbox client-side from a fresh snapshot. Returns {ok, value=target} (ok=false
when the target isn’t in the snapshot, isn’t rendered this frame, or the command post
fails — a stale/unrendered widget would otherwise drag from a defaulted (0,0) bbox).
steps scales the drag 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 first travels from the tracked previous 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. Accumulates a self-verification miss (surfaced as a loud panic at with_recording_app
teardown) rather than panicking mid-body — so the recording still stops + shuts the host
down cleanly. For force_set-driven tutorial stages (slider / drag / input / color); the
verified counterpart of the clicks hold_through_voice checks automatically. 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: the key
is held for hold_frames frames then released via imgui_key_play’s frame-index
pacing, so it fires its shortcut/navigation exactly once regardless of frame rate. The
old wall-clock press+release pair spanned a variable number of frames, so on a slow frame
(e.g. the recorder’s synchronous-APNG capture) the hold crossed ImGui’s key-repeat
threshold and over-fired — one Tab cycling several fields, one Enter committing twice.
Mirrors imgui_key_chord’s frame-pacing but for any key (no ASCII / modifier restriction).
Use int(ImGuiKey.Tab) / int(ImGuiKey.Enter) / etc. at the call site.
- 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, with a brief dwell pin
afterwards. Updates the tracked position. duration_ms is a FLOOR: the actual duration
scales with distance (≈0.325 px/ms ≈ constant cursor speed) so a long move doesn’t cover its
span in the same time as a short one and read as a teleport; short moves keep the floor.
- 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 / no infer), clearing @live state for a clean slate. ~100x cheaper
than reload; use between subtests of one feature instead of respawning the host.
Polls /status until the generation counter advances (a deterministic done
signal). Panics if the host never reports a baseline generation (e.g. a daslang-live
without POST /reset support), if a reset advanced generation but left has_error
set, or on 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.
Routes through imgui_set_window_size (regular window container);
the dispatcher feeds state.pending_size and window() calls
SetNextWindowSize on the next frame. SetNextWindowSizeConstraints
callbacks (snap/aspect/quantize) reshape the request — used by the
window_size_constraints recording driver to demonstrate exactly that.
Named resize_window (not set_window_size) to avoid the
overload-resolution collision with imgui_containers_builtin’s
in-frame set_window_size(size : float2; cond : ImGuiCond).
- 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 to settle, then stream
text as synthetic key+char events via imgui_key_type (into whichever widget holds
focus). Returns {ok, value=target} (ok=false on empty text or if focus never settles). duration_s > 0
paces typing over wall-clock; default uses imgui_key_type’s per-char cadence.
- Arguments:
app : ImguiApp
target : string
text : string
focus_max_frames : int
duration_s : float
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 an already-posted voice line, then assert watch_target’s
rendered state == expect_rendered — the reveal/dismiss rail. expect_rendered=true is a
REVEAL (a button that opens a popup/modal: watch a body widget appear); expect_rendered=false
is a DISMISS (a Yes/No/Apply/menu-item that closes the popup: watch a body widget — often the
click target itself — disappear). A button whose own click closes the popup can’t be verified by
its click-count (it’s gone next frame); the watch-target’s render state is the durable signal.
Self-verifying via record_check_rendered: a missed reveal/dismiss aborts the recording at
teardown. The caller should move_to the click target first. Every hold is PACING; the
reveal/dismiss is polled, not guessed.
- 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 (collapsing_header with closable=true,
or any closable strip whose header bbox is in telemetry) UNDER an already-posted voice line, then
assert the whole strip vanished by checking child_target is no longer rendered — a closable
header that closes stops drawing its entire body. The X sits a half header-height in from the
header’s right edge (mirrors how ImGui lays the close button out). Self-verifying via
record_check_rendered (expect=false): a missed close aborts the recording at teardown. The
caller should move_to the header first. Every hold is PACING; the close is polled, not guessed.
- 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 an already-posted voice
line, then assert target.value == item. Clicks the combo box (inset from the left so it
never lands on the trailing label), POLLS until the popup renders its per-item sub_bboxes
(combos are built as BeginCombo + a Selectable loop, so each item has a real rect), travels the
cursor onto item item via widget_component_point and clicks it, then verifies the
selection. Self-verifying: a missed open or wrong pick aborts the recording at teardown. Every
hold is PACING; the open and the commit are polled, not guessed.
- 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 CURRENT tracked cursor to to_pos UNDER
an already-posted voice line, then assert the drag CHANGED target.payload[field]. The caller
must have move_to’d onto the handle first — the press starts at the tracked cursor, so there
is no re-position jump. The lead scales with the voice line (~1/3 in, bounded 700..3000ms) so the
scrub lands WHILE the narrator describes it instead of always firing ~0.6s in — a continuous drag
is timing-sensitive in a way a point click is not. The handle scrubs slowly (constant ~0.30 px/ms
so short and long drags read the same), then the caption holds for the rest of the voice. The
effect is VERIFIED as a CHANGE from the pre-drag value (record_check_changed) — a no-op drag
(landed off, force_set removed, widget frozen) aborts the recording at teardown. drag_ms
overrides the auto-scaled duration when > 0. field is the payload key to watch — the default
"value" covers sliders/drags; docking passes "size" (splitter resize, where target is
a NEIGHBOR window) or "dock_id" (tab re-dock, where target is the dragged window itself),
since a dock_window has no value but its pos/size/dock node move under the drag.
- 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 lets the
box + voice start, then each click lands (ripple + state change visible while the voice
plays), then hold the caption for the rest of its voice. dwell is the value returned
by say_begin; pass the caption/voice LITERALS to say_begin at the call site (so
prepare_recording can scan them) and forward its result here. The caller must have already
moved the cursor onto the first target. Each click is VERIFIED (the effect must register,
or the recording aborts) and the tracked cursor is advanced to the click’s resolved point
so the next move_to starts there (no teleport). Multi-click stages dwell a beat between
picks so each state is visible.
- Arguments:
app : ImguiApp
dwell : uint
clicks : array<string>
Open a menu-bar menu (header_target, e.g. MAIN_BAR/FILE_MENU) and pick an item inside it
(item_target) UNDER an already-posted voice line, navigating the way a human does: click the
header, then drop STRAIGHT DOWN the menu’s column onto the item. A direct click on the item would
travel diagonally from the header across the menu bar and clip a SIBLING menu header — ImGui
hover-switches the open menu mid-travel and the click lands in the wrong menu. The vertical drop
stays inside the open menu’s column (menu items span the full menu width, so the header’s center-x
lands on the item) and never crosses a sibling. Self-verifying via record_check_rendered: the
item must render after the header click (menu opened) and stop rendering after the item click (pick
landed + menu closed), or the recording aborts at teardown. The caller should move_to the
header’s column (directly below the header, below the bar) first so the open-click travels vertically.
Every hold is PACING; the open/close are polled, not guessed.
- 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
per-series legend shown), a cross-widget invariant, any composed predicate. Pass the
boolean you computed (typically a wait_for_*(...) != null gate); on false it accumulates
desc into the recording’s failure ledger, surfaced as a teardown panic by
with_recording_app — same accumulate-don’t-panic discipline as the rest of the family.
app is unused (the caller did the polling) but kept for call-site symmetry. 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 (the value captured before the gesture) within max_frames. This is
the honest self-test for a continuous drag — a human drag lands on no exact pixel value, so we
assert the value CHANGED rather than equals a brittle pixel-derived target. Accumulate-on-miss
(surfaced as a teardown panic), mirroring record_check_value; drag_through_voice uses it.
Returns true once the value 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 that register under SYNTHESIZED path
keys rather than a caller-declared ident — drawlist primitives ([drawlist_prim], kind
"add_*"). Asserts the snapshot registry holds at least min_count entries whose kind
starts with kind_prefix within max_frames. record_check_rendered targets one ident;
this counts a kind family — the only honest signal when the call sites have no stable ident.
Accumulate-on-miss (surfaced as a teardown panic), mirroring record_check_rendered. Returns true on match.
- 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 within max_frames. The robust signal for a
container whose body is gated on open/close (tree_node / collapsing_header): collapsing a
tree stops its body drawing, so a child leaf flips to rendered:false — exactly the “body runs
only while open” behavior the widget teaches, and readable even when the container’s own
opened/flags are absent from the payload (@optional omits a false opened; an
OR-combined flag enum currently serializes to nothing). Accumulate-on-miss (surfaced as a
teardown panic), mirroring record_check_value; toggle_tree_voice uses it. Returns true on match.
- 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 - the mirror of record_check_changed.
Asserts target.payload[field]’s JSON still EQUALS before (the value captured before the
gesture). This is the honest self-test for a NEGATIVE claim - e.g. a node OUTSIDE a group does
NOT move when the group is dragged. Unlike record_check_changed there is no event to wait
for, so the caller must settle the gesture first (wait_for_mouse_idle); this then compares
once. Accumulate-on-miss (surfaced as a teardown panic), mirroring record_check_changed.
Returns true when the value is still before.
- 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. This is the public rail for self-verifying a NON-CLICK
effect in a recording — drive the real gesture (move_to to hover, drag / drag_to,
type_text) then call this to assert the effect landed (the hover status updated, the slider
value moved, the field took the text). Clicks already verify through hold_through_voice /
verify_click_effect, which call this internally; force_set_verified uses it too.
A recording body must NOT panic mid-run: daslang panic is fatal and can’t be recovered-and-continued,
so a mid-body panic would skip the clean record_stop + /shutdown and orphan the host. So on a
mismatch we push the same diagnostic expect_value would panic with onto g_record_failures
and return false; the body runs to its normal teardown, then with_recording_app fails loudly
after the host is drained. Returns true when the value matched.
- Arguments:
app : ImguiApp
target : string
field : string
expected : auto(T)
max_frames : int
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 for the dwell so the
recorder captures the on-screen window. The overlay’s duration_s is consumed on the
same clock as the hold (the app’s dt — content-time under recording), not a tick budget,
so it lingers exactly across the beat even though render fps and capture fps diverge. In voiceover mode the dwell is the
wav length (+gap) instead, so the caption stays up exactly while the voice speaks.
Named say (not narrate) to avoid overload-resolution churn with
imgui_visual_aids::narrate (the host-side overlay function), which differs
only in the first arg type (string vs ImguiApp). For a click/drag that must land under
the voice, use say_begin + a manual dwell split instead.
- 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 = "" )
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 milliseconds WITHOUT holding. The caller then holds the returned
dwell (content-time under recording) — splitting it around a click / drag so the
action lands visibly under the voice (cursor lands -> say_begin -> short lead -> click ->
hold the remainder). For plain narration with no mid-dwell action, use say which posts
+ holds in one call.
text is the on-screen caption; voice (when set) is the spoken line — write
captions for the eye (terse) and voice for the ear (natural sentences). Empty voice
falls back to text. For a step spanning sibling controls, use the targets overload.
- 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 (or collapsing_header) header UNDER an already-posted voice line, then
assert the toggle landed by checking child_target’s rendered state — the body-runs-only-while-open
signal (a collapsed node stops drawing its children). on_arrow=true clicks the chevron (a small
inset from the header’s left edge, scaled by header height); on_arrow=false clicks the header
center / label. The distinction is the whole point of OpenOnArrow: arrow toggles, label is a
no-op — drive both for real and verify each. Requires the header bbox in telemetry (tree_node
captures it via current_item_bbox since this rail). Self-verifying via record_check_rendered:
a missed toggle (or a label click that wrongly toggled) aborts the recording at teardown. The caller
should move_to the header first so the cursor is already near it. Every hold is PACING; the
toggle effect is polled, not guessed.
- 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: under an already-posted voice line, drive a REAL keyboard
edit of target and assert the committed value == expected. CLICKS click_pos to focus
the (first) field — a real click, NOT imgui_focus, because ImGui’s InputScalar AutoSelectAll
fires only on activation, so the click both focuses AND selects the existing text (typing then
REPLACES rather than inserts). Then per cells entry: type it, Tab to the next field BETWEEN
entries (vector inputs cycle their components on Tab), and finally Enter to commit. A scalar is
one cell (["12.5"]); a float3 is three. The lead scales with the voice (~1/3 in, bounded)
so the typing lands while the narrator describes it; the caption holds the rest. A wrong or
uncommitted value aborts the recording at teardown (record_check_value). The caller must have
move_to’d click_pos first so the cursor is already on the field (no teleport).
Every hold here is PACING; focus, focus-moved and value-committed are each POLLED, not guessed.
- Arguments:
app : ImguiApp
dwell : uint
target : string
click_pos : tuple<float;float>
cells : array<string>
expected : auto(T)
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. The content-clock replacement for a driver’s wall-clock hold_remainder. 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 seconds of video). No-op outside a recording session. The target is anchored on the first VALID status read (a transient null never lowers it), and a null read keeps polling rather than ending the hold early. If capture stalls (frames don’t advance for RECORD_STALL_US — the app isn’t rendering during this hold), degrade to a wall-clock sleep for the remaining span so the beat still lands and CI never hangs.
- Arguments:
app : ImguiApp
frames : int
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) — for every registered widget: its
ident, kind, rendered flag, bbox, and payload.value when present. The way to
see what a headless/CI run actually painted when you can’t watch the window; pair it
with a failed assertion (dump on the failure path) 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
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). When on, any wait_* helper that
gives up logs the live UI state once via log_snapshot before returning null / false,
so a red headless or slow-CI run is self-explaining with zero per-test dump code. Turn it
off around a negative wait — one where the timeout is the asserted, expected outcome
(e.g. confirming a widget never appears) — 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 it
returns true, up to timeout_sec real seconds; returns true on match, false on timeout.
The cmd_* sibling of wait_until_sec: for state gated on a command round-trip (sim
tick, camera mode, scene count) rather than a widget snapshot field. Replaces the hand-rolled
for (_ in range(N)) { if (cond) break } busy-poll, which bounds by frame count with no
sleep and so expires prematurely against the multi-kHz headless server (see wait_until).
- 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 snapshot until registered widget widget_ident’s hover state (IsItemHovered, the
per-widget hover field) equals expected. The sync gate for a reliable synthetic click:
move_to(point) then wait_for_hover(target) then click guarantees the press lands a
frame AFTER hover is established. Without it, a synthetic move``+``press can batch into one
windowed-vsync frame and the press fires before HoveredId is set — the press-before-hover
race a real continuous-hover mouse never hits (synthetic != real). Bounded by timeout_sec;
returns the matching snapshot or null on timeout. null is a real failure — gate it with
record_check (recording) or success (test); never proceed to the click silently.
The widget_exists guard is load-bearing for expected=false: an absent / misspelled
ident yields a null hover that ?? false would coalesce to false, so a missing
widget would false-pass as “not hovered” and mask a sync failure — same guard as
widget_rendered.
- 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 left from a prior appearance (e.g. a re-opened popup / menu) while the widget
is currently off-screen, so a click fired right after can dispatch before the widget
re-renders and be silently dropped (no bbox this frame). Use this whenever the click target
may have been shown before.
- 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 60 fps-equivalent server frames (max_frames/60 seconds).
Returns the matching snapshot or null on timeout.
The legacy semantics treated max_frames as “server frame counter delta”, which fired
prematurely under headless: the server advances g_frame at multi-kHz, so max_frames=240
could expire in ~24 ms — far short of any time-driven server work (timeline completion,
fixed-duration animations, ImGui state convergence). The translation to wall-clock at the
conventional 60 fps preserves the headed-mode budget every test was tuned against, without
the per-mode skew. For new code that’s natively expressed in seconds, prefer wait_until_sec.
- 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 it returns true,
up to timeout_sec real seconds. Returns the matching snapshot or null on timeout.
Prefer this over wait_until for tests gated on time-driven server work
(timeline completion, fixed-duration animations) — the frame-budget form fires
prematurely in headless mode where server frames run at multi-kHz with no vsync.
4.1.10. Pause control
- pause(app: ImguiApp )
POST /pause to halt the host’s update() (skips advance_coroutines + render).
POST /command continues to dispatch and POSTs still get drained by server.tick()
(which runs from the debug agent’s onTick, gated by auto_tick_agents — not by paused).
Useful for deterministic observation: spawn an L1 coroutine, probe imgui_await
while the world is frozen, then unpause to let it run.
- 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 per-frame advance otherwise races the next HTTP POST.
- Arguments:
app : ImguiApp
body : block<void>
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.