.. _stdlib_imgui_playwright: ======================================================================= Playwright — block-form test harness for daslang-live + dasImgui apps ======================================================================= .. das:module:: imgui_playwright 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 :ref:`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. +++++++++ Constants +++++++++ .. _global-imgui_playwright-DEFAULT_LIVE_PORT: .. das:attribute:: DEFAULT_LIVE_PORT = 9090 DEFAULT_LIVE_PORT:int const .. _global-imgui_playwright-DEFAULT_DEADLOCK_SANITY_SEC: .. das:attribute:: DEFAULT_DEADLOCK_SANITY_SEC = 120f DEFAULT_DEADLOCK_SANITY_SEC:float const .. _global-imgui_playwright-DEFAULT_READY_TIMEOUT_SEC: .. das:attribute:: DEFAULT_READY_TIMEOUT_SEC = 30f DEFAULT_READY_TIMEOUT_SEC:float const .. _global-imgui_playwright-DEFAULT_TEST_TIMEOUT_SEC: .. das:attribute:: DEFAULT_TEST_TIMEOUT_SEC = 120f DEFAULT_TEST_TIMEOUT_SEC:float const .. _global-imgui_playwright-DEFAULT_RELOAD_TIMEOUT_SEC: .. das:attribute:: DEFAULT_RELOAD_TIMEOUT_SEC = 30f DEFAULT_RELOAD_TIMEOUT_SEC:float const .. _global-imgui_playwright-DEFAULT_FRAME_WAIT_SEC: .. das:attribute:: DEFAULT_FRAME_WAIT_SEC = 10f DEFAULT_FRAME_WAIT_SEC:float const .. _global-imgui_playwright-READY_POLL_INTERVAL_MS: .. das:attribute:: READY_POLL_INTERVAL_MS = 0x64 READY_POLL_INTERVAL_MS:uint const .. _global-imgui_playwright-FRAME_POLL_INTERVAL_MS: .. das:attribute:: FRAME_POLL_INTERVAL_MS = 0x32 FRAME_POLL_INTERVAL_MS:uint const ++++++++++ Structures ++++++++++ .. _struct-imgui_playwright-ImguiApp: .. das:attribute:: ImguiApp struct ImguiApp +++++++++++++ App lifecycle +++++++++++++ * :ref:`with_imgui_app (feature_path: string; body: block\<(app:ImguiApp):void\>) ` * :ref:`with_imgui_app_opt (feature_path: string; port: int; ready_timeout_sec: float; test_timeout_sec: float; body: block\<(app:ImguiApp):void\>) ` .. _function-imgui_playwright_with_imgui_app_string_block_ls_app_c_ImguiApp_c_void_gr_: .. das:function:: 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: :ref:`ImguiApp `):void> .. _function-imgui_playwright_with_imgui_app_opt_string_int_float_float_block_ls_app_c_ImguiApp_c_void_gr_: .. das:function:: 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 ``; 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: :ref:`ImguiApp `):void> ++++++++++++++++++ Locators / queries ++++++++++++++++++ * :ref:`find_widget (var snap: JsonValue?; ident: string) : JsonValue? ` * :ref:`widget_exists (var snap: JsonValue?; ident: string) : bool ` * :ref:`widget_payload_field (var snap: JsonValue?; ident: string; field: string) : JsonValue? ` * :ref:`widget_rendered (var snap: JsonValue?; ident: string) : bool ` .. _function-imgui_playwright_find_widget_JsonValue_q__string: .. das:function:: 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** : :ref:`JsonValue `? * **ident** : string .. _function-imgui_playwright_widget_exists_JsonValue_q__string: .. das:function:: 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** : :ref:`JsonValue `? * **ident** : string .. _function-imgui_playwright_widget_payload_field_JsonValue_q__string_string: .. das:function:: 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** : :ref:`JsonValue `? * **ident** : string * **field** : string .. _function-imgui_playwright_widget_rendered_JsonValue_q__string: .. das:function:: 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** : :ref:`JsonValue `? * **ident** : string +++++++ Actions +++++++ * :ref:`click (app: ImguiApp; target: string) : JsonValue? ` * :ref:`close_widget (app: ImguiApp; target: string) : JsonValue? ` * :ref:`drag (app: ImguiApp; target: string; dx: float; dy: float; steps: int = 4; button: int = 0) : JsonValue? ` * :ref:`focus (app: ImguiApp; target: string) : JsonValue? ` * :ref:`open_widget (app: ImguiApp; target: string) : JsonValue? ` * :ref:`reload (app: ImguiApp; timeout_sec: float = DEFAULT_RELOAD_TIMEOUT_SEC) ` * :ref:`set_value (app: ImguiApp; target: string; var value: JsonValue?) : JsonValue? ` * :ref:`type_text (app: ImguiApp; target: string; text: string; focus_max_frames: int = 120; duration_s: float = 0f) : JsonValue? ` .. _function-imgui_playwright_click_ImguiApp_string: .. das:function:: click(app: ImguiApp; target: string) : JsonValue? Synthesize a click on ``target`` (``imgui_click`` verb). :Arguments: * **app** : :ref:`ImguiApp ` * **target** : string .. _function-imgui_playwright_close_widget_ImguiApp_string: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string .. _function-imgui_playwright_drag_ImguiApp_string_float_float_int_int: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string * **dx** : float * **dy** : float * **steps** : int * **button** : int .. _function-imgui_playwright_focus_ImguiApp_string: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string .. _function-imgui_playwright_open_widget_ImguiApp_string: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string .. _function-imgui_playwright_reload_ImguiApp_float: .. das:function:: 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** : :ref:`ImguiApp ` * **timeout_sec** : float .. _function-imgui_playwright_set_value_ImguiApp_string_JsonValue_q_: .. das:function:: 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: * **app** : :ref:`ImguiApp ` * **target** : string * **value** : :ref:`JsonValue `? .. _function-imgui_playwright_type_text_ImguiApp_string_string_int_float: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string * **text** : string * **focus_max_frames** : int * **duration_s** : float +++++++++ Snapshots +++++++++ * :ref:`expect_render (app: ImguiApp; widget_ident: string; timeout_sec: float = DEFAULT_FRAME_WAIT_SEC) ` * :ref:`expect_value (app: ImguiApp; target: string; field: string; expected: auto(T); max_frames: int = 300) ` * :ref:`snapshot (app: ImguiApp) : JsonValue? ` .. _function-imgui_playwright_expect_render_ImguiApp_string_float: .. das:function:: 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** : :ref:`ImguiApp ` * **widget_ident** : string * **timeout_sec** : float .. _function-imgui_playwright_expect_value_ImguiApp_string_string_autoT_int_0x13b: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string * **field** : string * **expected** : auto(T) * **max_frames** : int .. _function-imgui_playwright_snapshot_ImguiApp: .. das:function:: snapshot(app: ImguiApp) : JsonValue? Fetch the full widget snapshot (``imgui_snapshot`` command) as a parsed ``JsonValue``. :Arguments: * **app** : :ref:`ImguiApp ` +++++++++++++++ Polling / await +++++++++++++++ * :ref:`await_probe (app: ImguiApp; until: string = "quiescent") : JsonValue? ` * :ref:`await_quiescent (app: ImguiApp; timeout_sec: float = 5f) ` * :ref:`wait_for_payload_value (app: ImguiApp; target: string; field: string; expected: auto(T); max_frames: int = 300) : bool ` * :ref:`wait_until (app: ImguiApp; max_frames: int; pred: block\<(var snap:JsonValue?):bool\>) : JsonValue? ` .. _function-imgui_playwright_await_probe_ImguiApp_string: .. das:function:: 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** : :ref:`ImguiApp ` * **until** : string .. _function-imgui_playwright_await_quiescent_ImguiApp_float: .. das:function:: 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** : :ref:`ImguiApp ` * **timeout_sec** : float .. _function-imgui_playwright_wait_for_payload_value_ImguiApp_string_string_autoT_int_0x12c: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string * **field** : string * **expected** : auto(T) * **max_frames** : int .. _function-imgui_playwright_wait_until_ImguiApp_int_block_ls_var_snap_c_JsonValue_q__c_bool_gr_: .. das:function:: 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: * **app** : :ref:`ImguiApp ` * **max_frames** : int * **pred** : block<(snap: :ref:`JsonValue `?):bool> ++++++++++++++++++ Transport plumbing ++++++++++++++++++ * :ref:`post_command (app: ImguiApp; name: string; var args: JsonValue?) : JsonValue? ` .. _function-imgui_playwright_post_command_ImguiApp_string_JsonValue_q_: .. das:function:: post_command(app: ImguiApp; name: string; args: JsonValue?) : JsonValue? Send ``{"name":,"args":}`` through ``app.transport``; returns parsed JSON or null on transport failure. Escape hatch for custom verbs outside the shipped helper set. :Arguments: * **app** : :ref:`ImguiApp ` * **name** : string * **args** : :ref:`JsonValue `? +++++++++++++ Uncategorized +++++++++++++ .. _function-imgui_playwright_reset_cursor_pos_tuple_ls_float;float_gr_: .. das:function:: reset_cursor_pos(pos: tuple = tuple>(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 .. _function-imgui_playwright_move_to_ImguiApp_tuple_ls_float;float_gr__int: .. das:function:: move_to(app: ImguiApp; pos: tuple; 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** : :ref:`ImguiApp ` * **pos** : tuple * **duration_ms** : int .. _function-imgui_playwright_click_at_array_ls_JsonValue_q__gr__int_tuple_ls_float;float_gr__int_int: .. das:function:: click_at(events: array; t_ms: int; pos: tuple; 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< :ref:`JsonValue `?> * **t_ms** : int * **pos** : tuple * **travel_ms** : int * **button** : int .. _function-imgui_playwright_drag_along_array_ls_JsonValue_q__gr__int_tuple_ls_float;float_gr__tuple_ls_float;float_gr__int_int_int: .. das:function:: drag_along(events: array; t_ms: int; from_pos: tuple; to_pos: tuple; 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< :ref:`JsonValue `?> * **t_ms** : int * **from_pos** : tuple * **to_pos** : tuple * **drag_ms** : int * **approach_ms** : int * **button** : int .. _function-imgui_playwright_wait_until_ready_ImguiApp_float: .. das:function:: wait_until_ready(app: ImguiApp; timeout_sec: float) : bool Poll GET /status until 200 OK or timeout. Returns true on ready. :Arguments: * **app** : :ref:`ImguiApp ` * **timeout_sec** : float .. _function-imgui_playwright_get_text_ImguiApp_string: .. das:function:: get_text(app: ImguiApp; uri: string) : string GET `uri`; return the raw response body or "". :Arguments: * **app** : :ref:`ImguiApp ` * **uri** : string .. _function-imgui_playwright_wait_until_sec_ImguiApp_float_block_ls_var_snap_c_JsonValue_q__c_bool_gr_: .. das:function:: 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: * **app** : :ref:`ImguiApp ` * **timeout_sec** : float * **pred** : block<(snap: :ref:`JsonValue `?):bool> .. _function-imgui_playwright_wait_for_widget_ImguiApp_string_float: .. das:function:: 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** : :ref:`ImguiApp ` * **widget_ident** : string * **timeout_sec** : float .. _function-imgui_playwright_wait_for_render_ImguiApp_string_float: .. das:function:: 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** : :ref:`ImguiApp ` * **widget_ident** : string * **timeout_sec** : float .. _function-imgui_playwright_wait_for_int_value_ImguiApp_string_string_int_int: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string * **field** : string * **expected** : int * **max_frames** : int .. _function-imgui_playwright_wait_for_bool_value_ImguiApp_string_string_bool_int: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string * **field** : string * **expected** : bool * **max_frames** : int .. _function-imgui_playwright_wait_for_string_value_ImguiApp_string_string_string_int: .. das:function:: 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** : :ref:`ImguiApp ` * **target** : string * **field** : string * **expected** : string * **max_frames** : int .. _function-imgui_playwright_drag_to_ImguiApp_string_string_int_int: .. das:function:: 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** : :ref:`ImguiApp ` * **source** : string * **target** : string * **steps** : int * **button** : int .. _function-imgui_playwright_wait_for_key_idle_ImguiApp_float: .. das:function:: 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** : :ref:`ImguiApp ` * **timeout_sec** : float .. _function-imgui_playwright_wait_for_mouse_idle_ImguiApp_float: .. das:function:: 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** : :ref:`ImguiApp ` * **timeout_sec** : float .. _function-imgui_playwright_pause_ImguiApp: .. das:function:: 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** : :ref:`ImguiApp ` .. _function-imgui_playwright_unpause_ImguiApp: .. das:function:: unpause(app: ImguiApp) POST ``/unpause`` to resume the host's update(). :Arguments: * **app** : :ref:`ImguiApp ` .. _function-imgui_playwright_with_paused_ImguiApp_block_ls__c_void_gr_: .. das:function:: 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** : :ref:`ImguiApp ` * **body** : block