@unotest/web 0.5.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,297 @@
1
+ ---
2
+ status: stable
3
+ last_updated: 2026-05-14
4
+ ---
5
+
6
+ # Agent integration
7
+
8
+ `@unotest/web` ships an MCP server with **39 tools**. This manual covers
9
+ how to wire it into common MCP clients and what the agent can do once
10
+ connected.
11
+
12
+ ## MCP client configuration
13
+
14
+ ### Claude Code
15
+
16
+ `.mcp.json` (repo root) or `~/.claude/.mcp.json`:
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "unotest-web": {
22
+ "command": "npx",
23
+ "args": ["@unotest/web", "mcp"]
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ Claude Code prompts for approval on first start. After approve, the
30
+ 39 `mcp__unotest-web__*` tools appear in the agent's tool surface.
31
+
32
+ ### Claude Desktop
33
+
34
+ `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "unotest-web": {
40
+ "command": "npx",
41
+ "args": ["@unotest/web", "mcp"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Cursor
48
+
49
+ Cursor's MCP config follows the same shape — see Cursor docs for the
50
+ file location.
51
+
52
+ ### Verify manually
53
+
54
+ ```sh
55
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
56
+ | npx @unotest/web mcp
57
+ ```
58
+
59
+ Should return JSON with `tools: [...39 entries]`.
60
+
61
+ ## Tool catalog
62
+
63
+ ### Core browser actions (22)
64
+
65
+ Direct equivalents of the DSL surface — usable when the agent wants to
66
+ drive the page outside a `run_test` (e.g., exploratory navigation,
67
+ manual setup).
68
+
69
+ `new_context`, `close_context`, `goto`, `reload`, `get_url`, `get_title`,
70
+ `click`, `double_click`, `fill`, `press`, `check`, `uncheck`,
71
+ `select_option`, `hover`, `scroll_into_view`, `wait_for`, `wait_for_url`,
72
+ `wait_for_text`, `get_page_snapshot`, `get_frame_snapshot`,
73
+ `get_aria_snapshot`, `find_element`.
74
+
75
+ `get_page_snapshot` / `get_frame_snapshot` return a compact outline of
76
+ **interactive** elements (action surface). `get_aria_snapshot` returns
77
+ a YAML ARIA tree of the page including **non-interactive** named nodes
78
+ (headings, regions, lists, paragraphs, dialogs, images) plus free text
79
+ — the verification surface. Refs are shared: the same `[ref=eN]`
80
+ resolves the same element across all three tools. Format mirrors
81
+ Playwright `ariaSnapshot` AI mode so agents trained on playwright-mcp
82
+ output need no retraining.
83
+
84
+ `find_element({role, name?, near?, nth?})` — targeted ARIA search.
85
+ Returns matching element refs without dumping a full snapshot — the
86
+ escape hatch when `get_page_snapshot` / `get_aria_snapshot` exceed
87
+ the MCP token cap (table-heavy fleet UIs with 100+ rows). Refs are
88
+ shared with outline / aria-snapshot.
89
+
90
+ ### Debugger (6)
91
+
92
+ The reason most agents pick this package over driving Playwright
93
+ directly. Detailed workflow in `debugger-tools.md`.
94
+
95
+ - `run_test` — load `unotest/e2e/<name>.js`, start an AST execution.
96
+ Modes: `auto` (run to completion / failure), `step` (pause after every
97
+ statement). `pauseOnFailure` defaults to `true`.
98
+ - `step` — advance one AST node. Returns the next event.
99
+ - `resume` — run until next pause / completion / failure.
100
+ - `inspect_runtime` — snapshot of runtime state: current line, last
101
+ event, last failure (with line/col + error), and all local variables.
102
+ - `abort_runtime` — kill the runtime, release the browser context.
103
+ - `list_runtimes` — recover a lost `runtimeId`.
104
+
105
+ ### Multi-context (4)
106
+
107
+ - `list_pages` — open tabs/windows in the active context
108
+ - `list_frames` — iframe tree
109
+ - `get_active_context` — current page index + URL + frame depth
110
+ - `switch_page` — change active tab
111
+
112
+ #### Popup auto-switch (recording only)
113
+
114
+ In `explore_step` the MCP server watches for tabs/popups opened by the
115
+ action during a short grace window. If one appears, the server:
116
+
117
+ 1. Auto-switches the active tab to it (last-opened wins on multi-popup).
118
+ 2. Appends a synthetic `set_page` entry to the recording log so the
119
+ generated DSL contains `setPage(N);` and replays correctly in
120
+ `run_test`. If a frame scope was active before the switch, synthetic
121
+ `exit_frame` entries are appended first (frame stack does not survive
122
+ tab changes).
123
+ 3. Adds `autoSwitched: { from, to, url, totalNewPages }` to the
124
+ `explore_step` JSON response so the agent's next snapshot targets the
125
+ new tab without confusion.
126
+
127
+ If popups appeared but every one closed before the switch could land,
128
+ the response carries `autoSwitchAttempted: { collected, allClosed: true }`
129
+ instead.
130
+
131
+ Tuning via `unotest/.env`:
132
+
133
+ ```
134
+ # Max grace window (ms) — popup-watcher early-exits ~200 ms after the
135
+ # first popup event lands, so the full cap is only paid when no popup
136
+ # arrives. 0 disables. Default 2000. Range [0, 10000].
137
+ UNOTEST_POPUP_GRACE_MS=2000
138
+ ```
139
+
140
+ Auto-switch runs **only** under `explore_step` (recording / ad-hoc).
141
+ `run_test` stays strict — if the author omits `setPage`, the test fails.
142
+
143
+ ### Failure bundle introspection (6)
144
+
145
+ - `list_failures` — bundles on disk, newest first
146
+ - `get_failure_trace` — `failure.json` + error + line/col
147
+ - `get_failure_console` — captured console entries (last 100)
148
+ - `get_failure_a11y` — semantic-DOM snapshot at failure moment
149
+ - `get_failure_screenshot` — PNG (returned as base64; tool returns the
150
+ path otherwise)
151
+ - `get_failure_network` — HAR (tier 3 — not yet wired, see
152
+ `KNOWN_ISSUES.md`)
153
+
154
+ ### `agent_fix` (1)
155
+
156
+ Build a structured fix-context for a failed run (D-25). Composes the
157
+ last failure + relevant DOM + scenario excerpt + suggested action
158
+ classification. **Does not call an LLM** — the agent receiving the
159
+ bundle decides what to do. Detailed in `agent-fix.md`.
160
+
161
+ ### `get_last_mcp_log` (1)
162
+
163
+ Read the server's own diagnostic log instead of guessing from a
164
+ truncated error. Gated on `UNOTEST_DEBUG` (see [Debug logging](#debug-logging));
165
+ when off it returns an actionable hint. No args → the full last tool-call
166
+ with its nested resolver / driver / snapshot events. `filter:"errors"` →
167
+ the last failed call plus recent failing exits. `filter:"layer:resolver"`
168
+ (or driver / snapshot / refs / tool) → recent events from that layer.
169
+ `filter:"tool:explore_step"` → recent events for that tool. `limit` caps
170
+ the count (default 50).
171
+
172
+ ## Debug logging
173
+
174
+ Set `UNOTEST_DEBUG=1` in `unotest/.env` and restart the MCP server to
175
+ record a structured **JSONL** trace plus **per-call folders with
176
+ ground-truth artifacts** under `unotest/.debug/`:
177
+
178
+ - `tool` — each MCP call: enter (args) + exit (ok / error / timing).
179
+ - `resolver` — ref → stable-locator resolution: the element hint, the
180
+ ranked candidates, and each attempt's match count + verdict.
181
+ - `driver` — Playwright actions: op, locator, timeout, and on failure the
182
+ tail of Playwright's call log (what intercepted the click, etc.).
183
+ - `snapshot` — every `get_*_snapshot` / `find_element`: byte size, ref
184
+ count, timing.
185
+ - `refs` — ref totals + counter ceiling per snapshot.
186
+
187
+ Every event carries `trace` (the explorationId, or the callId for ad-hoc
188
+ calls), `span` (the callId), and — for ref locators — `sourceSnapshot`, the
189
+ snapshot that minted the ref (its provenance / "before" state).
190
+
191
+ **Ground-truth artifacts** answer "did the snapshot glitch?" and "did the
192
+ locator hit the right element?" without guessing — each tool call gets a
193
+ folder:
194
+
195
+ ```
196
+ unotest/.debug/
197
+ <pid>-<isoStart>/ # one folder per process (session)
198
+ log.jsonl # the event stream (rotation-aware)
199
+ index.jsonl # one line per completed tool call
200
+ <NNN>-<tool>/ # one folder per tool call, seq-ordered
201
+ call.json # args + nested events + result
202
+ before.html after.html # full DOM before/after an action (read-only)
203
+ before.txt after.txt # readable outline before/after (read-only)
204
+ diff.txt # line diff before.txt↔after.txt — what changed
205
+ aria.yaml | outline.txt | find_element.json # the FULL snapshot, rendered
206
+ resolved/<ref>.html # outerHTML of what the locator matched
207
+ page.html screenshot.png # failure forensic
208
+ mcp-latest.jsonl # symlink to the active log (tail -f)
209
+ last-call.json # the last tool call + its nested events
210
+ last-error.json # the last FAILED tool call
211
+ ```
212
+
213
+ For every action (`explore_step` / `explore_record`) the proxy writes a
214
+ **read-only** `before.*`/`after.*` pair — `before.html`/`after.html` (full
215
+ DOM, `data-unotest-ref` baked in) and `before.txt`/`after.txt` (readable
216
+ outline) plus `diff.txt` (a line diff of the two outlines — read this first:
217
+ it shows at a glance what the action changed, e.g. `value=""` → `value="…"`,
218
+ 303 rows appearing, or `(no change)` for a no-op). Unconditional (success,
219
+ no-op, or failure), so you can always see what the action changed and check
220
+ whether a ref was even on the page. These captures never mint/sweep refs, so
221
+ they can't change behaviour.
222
+
223
+ The rendered snapshot (`aria.yaml` / `outline.txt` — the same text the tool
224
+ returns) is the verification surface, never truncated. `resolved/<ref>.html`,
225
+ `page.html`, and `before/after.html` are full DOM (not capped); the
226
+ `UNOTEST_DEBUG_HTML_MAX_BYTES` (64 KB) cap applies to the failure-forensic
227
+ `page.html` only.
228
+ Old call folders are pruned beyond `UNOTEST_DEBUG_KEEP_CALLS` (200).
229
+
230
+ Read it from inside the agent with `get_last_mcp_log` (`filter:"network"`
231
+ and `filter:"exploration:<id>"` also work), or `tail -f
232
+ unotest/.debug/mcp-latest.jsonl` / `jq` from a shell. `UNOTEST_DEBUG=summary`
233
+ keeps only the `tool` + `snapshot` layers and writes no artifacts. Off by
234
+ default — zero I/O when unset.
235
+
236
+ > Network capture (a 6th `network` layer for failed / 4xx-5xx / navigation
237
+ > events) is planned; `filter:"network"` is accepted but currently empty.
238
+
239
+ ### Auditing a run against the rules — `audit_last_run`
240
+
241
+ `audit_last_run` runs a deterministic rubric (no LLM) over the recorded
242
+ trajectory and reports where the testing rules were broken — a self-check
243
+ the agent can run after a flow:
244
+
245
+ - **violation** — something to fix in how the test was built: a ref acted on
246
+ with no preceding snapshot (stale/guessed), the same ref failing to
247
+ resolve 3+ times (stuck), a recorded step missing its `section`/`description`.
248
+ - **report** — a signal you should see but didn't necessarily cause: a
249
+ locator forced to fall back to brittle text/CSS (no testid/role+name), or
250
+ an `allowNoRef` escape. (Whether an action actually changed anything is
251
+ read off the `before.*`/`after.*` captures, not inferred.)
252
+
253
+ Pass `exploration:"<id>"` to scope to one recording session; omit to audit
254
+ the whole process log.
255
+
256
+ **PII warning.** Tool arguments are logged **verbatim**, so a
257
+ `fill(value: "<password>")` lands in the log. It's a local, gitignored
258
+ file that is never uploaded — but if that's a concern, keep `UNOTEST_DEBUG`
259
+ off. No redaction is applied.
260
+
261
+ ## Recommended system prompt
262
+
263
+ The shipped agent prompt is in
264
+ `src/mcp/prompts/agent-test-author.md`.
265
+ Two-line summary for your custom system prompt if you want to integrate
266
+ without copy-paste:
267
+
268
+ > When writing or debugging `@unotest/web` scenarios, prefer
269
+ > `getByTestId` → `getByRole(name)` → `getByLabel` → `getByText` →
270
+ > `locator(css)`. When a scenario fails, call `inspect_runtime` for
271
+ > local-variable state before calling `step` / `resume`. Prefer typed
272
+ > getters (`getInnerText`, `getAttribute`) over `evaluate(js)`.
273
+
274
+ ## Capability model
275
+
276
+ The MCP server only acts on the consumer project it was launched from:
277
+ its cwd anchors `unotest/`, `unotest.config.*`, `.unotest/failures/`,
278
+ and the sandbox primitives.
279
+
280
+ What the server **cannot** do (by design — D-25):
281
+
282
+ - Call external LLM APIs
283
+ - Hold or load API keys
284
+ - Apply patches to your scenarios or source code automatically
285
+ - Access files outside the project root
286
+
287
+ What it **can** do (with your approval):
288
+
289
+ - Run your scenarios + drive browsers
290
+ - Read failure artifacts from `.unotest/failures/`
291
+ - Execute sandbox primitives — `shell(cmd, …args)` (execFile, not
292
+ shell), `dbQuery` / `dbExec` (against `sandbox.database`), `apiCall`
293
+ (path-only against `sandbox.apiBaseUrl`)
294
+
295
+ Pinning `sandbox.*` in `unotest.config.*` is how you constrain what the
296
+ agent's scenarios can touch: a scenario cannot redirect `shell` cwd or
297
+ hit an arbitrary HTTP host.