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:
widget_exists(snap: JsonValue?; ident: string ): bool

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

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

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

Arguments:
  • snap : JsonValue?

  • ident : string

  • field : string

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

True iff globals[ident] 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:

4.1.5. Actions

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

Synthesize a click on target (imgui_click verb).

Arguments:
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:
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:
open_widget(app: ImguiApp; target: string ): JsonValue?

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

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

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

Arguments:
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.

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

Auto-focus target, wait up to focus_max_frames for focus 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:

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:
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:
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.

Arguments:

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.

Arguments:

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:
get_text(app: ImguiApp; uri: string ): string

GET uri; return the raw response body or “”.

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

Wall-clock variant of wait_until: poll pred(snap) until 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.

Arguments:
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:
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:
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:
unpause(app: ImguiApp )

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

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

Scope: pause the host, invoke body, unpause. Body observes a frozen world — coroutines spawned by imgui_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: