Assert UI state
How to verify what an app shows — in single calls an agent can branch on, instead of fragile read-compare-retry loops.
Address elements: refs first, selectors when you know the DOM
A snapshot (electron_snapshot) assigns every interactive element a numbered ref, tagged onto
the DOM as data-sw-ref="N" and stable across re-renders of the same element. electron_find
resolves role + accessible name to a ref without any DOM knowledge:
electron_find { "role": "button", "name_contains": "Save" }
Most element-targeted assertion and interaction tools accept ref OR selector (never both);
count and URL assertions are exceptions because they target a query or a window rather than one
element. Prefer refs in agent flows: they survive markup refactors that break CSS selectors, and
when one does go stale the error carries recovery data instead of a dead end:
REF_NOT_FOUNDincludessimilar_refs— current elements that resemble the one you meant.- A find that matches nothing returns
hints: close names, matches avisible/enabled/interactivefilter hid, the name under other roles, and elements whose text content (not accessible name) holds it, with a selector forelectron_get_text/electron_exists. - Every snapshot and find response carries
renderer_reloaded; whentrue, the renderer reloaded since your last read and ALL refs are suspect — take a fresh snapshot.
The expect_* family — read + compare + retry in one round-trip
Each expect_* tool polls server-side until its predicate holds or timeoutMs (default 5000 ms,
0 = check once) expires:
| Tool | Asserts |
|---|---|
electron_expect_text |
Element text: equals / contains / regex / not_equals / not_contains |
electron_expect_value |
Form control value |
electron_expect_visible |
Visibility (or visible: false for absence) |
electron_expect_count |
How many elements match a selector / role+name query |
electron_expect_state |
A composite state object (checked, disabled, focused, …) |
electron_expect_url |
The renderer URL |
electron_assert_pattern |
One-shot regex over text — no polling, for "is it already so" |
electron_expect_text { "selector": "#status", "regex": "^Hello, .+!", "flags": "i" }
Regex predicates accept the i, m, s, u flags; the stateful g and y are rejected with
BAD_ARGUMENT rather than producing call-order-dependent results.
On failure the envelope is designed for the agent's next decision: EXPECTATION_FAILED is
retryable and its details carry expected AND actual, so the agent knows whether to wait
longer, fix its input, or report a real regression — without an extra read.
The wait_* family — synchronise, then act
| Tool | Waits for |
|---|---|
electron_wait_for_selector |
An element to be attached / visible / hidden / detached |
electron_wait_for_state |
A composite state object on one element |
electron_wait_for_event |
A named DOM event (transitionend, load, a custom event) on an element or the document |
electron_wait |
A fixed pause — last resort |
One sharp edge: intentionally offscreen or aria-hidden elements (a code editor's hidden
textarea, for instance) are never "visible" — wait for state: "attached" instead, or the wait
times out by design. The expiry is WAIT_TIMEOUT (retryable), and wait_for_state reports the
last state it observed so the agent sees how close it got. Typing into that editor has its own
reliable path — see Type into code editors.
Optional MCP progress during long calls
When an MCP host requests standard progress for a tool call, fixed waits, predicate waits, and
polling expectations can emit advisory elapsed-time updates. Each update uses the operation's
clamped timeout as its total and a stable phase such as Waiting for condition or
Checking expectation. Fast calls and checks with timeoutMs: 0 remain silent.
Other operations whose duration has no honest numeric total use semantic phases instead. Lifecycle calls and long plugin work can report transitions such as connecting, registering a session, capturing, comparing, or replaying. Pre-threshold transitions coalesce, and the same request-wide notification cap applies across nested calls.
There is no tool-level progress argument to add. The host opts in through MCP request metadata,
and it may ignore notifications even when requested. Cancelling the request stops progress,
propagates cancellation to handlers and nested calls, and cleans up pending session initialization.
The dispatch timeout triggers the same cleanup. Apps started by a cancelled launch are stopped;
existing apps being attached or injected remain running. Fixed-wait timers are released immediately,
and role-count expectations stop scheduling accessibility re-walks. A walk that was already running
in the renderer when the request was cancelled has renumbered the element refs, so its result is
still reconciled and stored once it returns. Walks on one renderer surface are serialized, so a
snapshot, find, ref diagnosis or ref-targeted action, read, wait or expectation started in the
meantime waits for that reconciliation and its refs stay valid. A selector that is exactly a ref's
[data-sw-ref="N"] form counts as that ref; other selector targets do not wait. A ref wait or
expectation holds that turn only while it resolves the ref, so a parallel ref action that
satisfies it is not blocked. If a walk renumbers the refs during the wait, the wait rechecks the ref
after the walk settles and continues with the remaining budget; a ref the new snapshot dropped
reports REF_NOT_FOUND. A walk that keeps the renderer busy for more than 60 seconds makes queued
calls fail with the retryable RENDERER_BUSY instead of waiting behind it.
Cancellation cannot interrupt or undo JavaScript already sent to Electron, including a renderer
poll. An operation on an existing session may still finish, so inspect app state before retrying a
mutation. A cancelled launch handshake is cleaned up when its transport returns a session. Plugin
handlers must cooperate with the cancellation signal between steps. Direct dispatch reports
OPERATION_CANCELLED; the MCP client handles its own cancelled request. Progress is advisory and
never proves that an operation completed.
Watching change: snapshot diffs
After an action, you rarely need the whole tree again:
electron_snapshot { "since": "last" }
The response is the delta — added, removed, and changed entries plus a ref_map — in a
compact encoding that carries only the fields that actually changed (pass
diffFormat: "full" for complete before/after entries). budgetTokens caps the estimated
JSON cost of the added, removed, and changed arrays together, including their keys,
brackets, and commas. It uses the server's character-count / 4 heuristic, not a model-specific
tokenizer, and excludes ref_map, metadata, and the response envelope. The server drops
lowest-value entries first and reports how many under _meta.truncated_entries. Over a multi-turn
session this is the difference between re-reading thousands of tokens per turn and reading tens.
Looking at one region: scoped snapshots
When only a dialog, panel, or list matters, scope the snapshot to it:
electron_snapshot { "root": { "selector": "[role=dialog]" } }
Only entries inside that element are returned; refs are the same ones a full snapshot would
show, so they work with every other tool. Pass root: { "ref": N } for an interactive container
such as a listbox (landmarks and dialogs have no ref, so use a selector). root combines with
since: "last", interactiveOnly, maxEntries, and format: "text". On the bench app, scoping
to the greeting form returns about 10% of the full snapshot's tokens.
Putting it together
A robust act-then-verify beat looks like:
electron_find→ ref.- Act (
electron_click { ref }). electron_expect_text/electron_expect_statewith a boundedtimeoutMs.- On
EXPECTATION_FAILED: inspectactual; onREF_NOT_FOUND: usesimilar_refsor re-find; onrenderer_reloaded: true: fresh snapshot, then retry once.
Design background: stable refs and the snapshot/diff schema are ADR-005; failure envelopes,
retryability, and EXPECTATION_FAILED semantics are ADR-006; the single-round-trip assertion
principle is ADR-007; request-scoped MCP progress is ADR-008. The model behind refs, snapshots,
and retrying assertions:
Concepts.