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; with_imgui_app_opt is the
variant that returns instead of panicking on launch failure.
Locators (find_widget, widget_exists, widget_payload_field,
widget_rendered) resolve via the same path keys the boost registry
uses; actions (click, type_text, 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
- 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
- FRAME_POLL_INTERVAL_MS = 0x32
FRAME_POLL_INTERVAL_MS:uint const
4.1.2. Structures
- ImguiApp
struct ImguiApp
4.1.3. App lifecycle
- with_imgui_app(feature_path: string; body: block<(app:ImguiApp):void> )
Convenience form using the module-level default port + timeouts. For non-default knobs, use with_imgui_app_opt.
- Arguments:
feature_path : string
body : block<(app: ImguiApp):void>
- with_imgui_app_opt(feature_path: string; port: int; ready_timeout_sec: float; test_timeout_sec: float; body: block<(app:ImguiApp):void> )
Spawn daslang-live <feature_path>; 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.
Forwards --headless (read from this process’s get_user_args())
into the spawn so the harness’s headless arm fires inside the subprocess.
- Arguments:
feature_path : string
port : int
ready_timeout_sec : float
test_timeout_sec : float
body : block<(app: ImguiApp):void>
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:
snap : JsonValue?
ident : string
- 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] is for a widget that painted this frame.
Registered-but-not-yet-painted entries carry "rendered": false; rendered widgets omit the field (?? true covers absent).
- Arguments:
snap : JsonValue?
ident : string
4.1.5. Actions
- click(app: ImguiApp; target: string ): JsonValue?
Synthesize a click on target (imgui_click verb).
- Arguments:
app : ImguiApp
target : string
- 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
- drag(app: ImguiApp; target: string; dx: float; dy: float; steps: int = 4; button: int = 0 ): JsonValue?
Synthesize an L1 mouse-drag on target’s bbox center: press → steps interpolated moves → release. Settles in steps + 2 frames.
- Arguments:
app : ImguiApp
target : string
dx : float
dy : float
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
- 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
- set_value(app: ImguiApp; target: string; value: JsonValue? ): JsonValue?
Set target’s payload value remotely (imgui_set verb); value is the JSON form expected by the widget kind.
- 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 synthesize UTF-8 chars via L1 AddInputCharactersUTF8.
duration_s > 0 paces typing over wall-clock (server: total_frames = int(duration_s * 60)); default 0 keeps the 1-char/frame rate.
- Arguments:
app : ImguiApp
target : string
text : string
focus_max_frames : int
duration_s : float
4.1.6. 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
- snapshot(app: ImguiApp ): JsonValue?
Fetch the full widget snapshot (imgui_snapshot command) as a parsed JsonValue.
- Arguments:
app : ImguiApp
4.1.7. Polling / await
- 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
- 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_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.
4.1.8. 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.
4.1.9. Uncategorized
- 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>
- move_to(app: ImguiApp; pos: tuple<float;float>; duration_ms: int = 600 )
Post a lerped cursor move from the last tracked position to pos
over duration_ms, with a brief dwell pin afterwards. Updates the
tracked position.
- Arguments:
app : ImguiApp
pos : tuple<float;float>
duration_ms : int
- 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
- 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
- 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
- get_text(app: ImguiApp; uri: string ): string
GET uri; return the raw response body or “”.
- Arguments:
app : ImguiApp
uri : string
- 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.
- 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_for_render(app: ImguiApp; widget_ident: string; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC ): JsonValue?
Poll snapshot until globals[widget_ident] reports rendered:true (the widget painted this frame).
Use when you specifically need first-paint semantics — registered-but-not-rendered widgets pass wait_for_widget immediately.
- Arguments:
app : ImguiApp
widget_ident : string
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_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_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
- 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
- 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
- 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_drag / imgui_type_text 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>