@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.
- package/.claude/hooks/log-agent.sh +24 -0
- package/.claude/skills/{write-e2e-test.md → write-e2e-test/SKILL.md} +25 -18
- package/CHANGELOG.md +69 -0
- package/README.md +59 -84
- package/bin/unotest-web.js +10 -32
- package/dist/config/schema.d.ts +9 -9
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +25 -20
- package/dist/dsl/index.js +1 -1
- package/dist/linter-DzAzJpab.d.ts +21 -0
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.d.ts +10 -1
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init.js +1 -1
- package/dist/runner/install-chromium.js +1 -1
- package/dist/runner/prepare-fix.js +1 -1
- package/dist/runner/serve-fixture.js +1 -1
- package/dist/runner/web-runner-adapter.js +1 -1
- package/guides/agent-integration.md +297 -0
- package/guides/dsl-reference.md +670 -0
- package/package.json +30 -39
- package/src/mcp/prompts/agent-test-author.md +71 -1
|
@@ -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.
|