Electron Stagewrightdocs

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:

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:

  1. electron_find → ref.
  2. Act (electron_click { ref }).
  3. electron_expect_text / electron_expect_state with a bounded timeoutMs.
  4. On EXPECTATION_FAILED: inspect actual; on REF_NOT_FOUND: use similar_refs or re-find; on renderer_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.