@unotest/web 0.30.0 → 0.32.0
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/skills/write-e2e-test/SKILL.md +12 -0
- package/.claude/skills/write-e2e-test-ground/SKILL.md +12 -0
- package/CHANGELOG.md +383 -0
- package/README.md +66 -2
- package/bin/unotest-web.js +3 -1
- package/dist/config/schema.d.ts +15 -15
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +11 -3
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +54 -4
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.d.ts +22 -2
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +1 -1
- package/dist/{interfaces-BluheUKs.d.ts → interfaces-B34N4rfI.d.ts} +8 -0
- package/dist/legacy-layout-By62PkbP.d.ts +8 -0
- package/dist/linter-Yf4gPe5J.d.ts +36 -0
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.d.ts +14 -8
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init/chrome-probe.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/scaffold-workspace.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 +190 -14
- package/guides/dsl-reference.md +156 -9
- package/package.json +8 -8
- package/src/mcp/prompts/agent-test-author.md +11 -1
- package/types/unotest-dsl.d.ts +29 -5
- package/dist/linter-CM2CpIyW.d.ts +0 -21
|
@@ -604,11 +604,23 @@ the old browser context, then `run_test` again.
|
|
|
604
604
|
| `wait_for_url` | `value` (the pattern) | `options` |
|
|
605
605
|
| `enter_frame` | `locator` | |
|
|
606
606
|
| `exit_frame` | | |
|
|
607
|
+
| `screenshot` | | `name`, `fullPage`, `locator`, `outline`, `evidenceOnly` |
|
|
607
608
|
|
|
608
609
|
When recording (`explorationId` present): `description` + `section`
|
|
609
610
|
are required. Ad-hoc (no `explorationId`): no description/section,
|
|
610
611
|
no recording.
|
|
611
612
|
|
|
613
|
+
`screenshot` is the **evidence** step: it files a PNG under the session
|
|
614
|
+
(`.unotest/explorations/<explorationId>/screenshots/<NNN>-<name>.png`,
|
|
615
|
+
`adhoc/` without a session), replies with `{path, width, height}` and
|
|
616
|
+
the image itself, and becomes `screenshot(name)` in the saved test.
|
|
617
|
+
`name` defaults to the section slug; `locator` captures one element
|
|
618
|
+
(the saved test degrades that to a page capture); `outline: true` adds
|
|
619
|
+
the page outline to the reply; `evidenceOnly: true` keeps the file but
|
|
620
|
+
records no step. `explore_stop` lists every file in `artifacts`. Take
|
|
621
|
+
one wherever a human would want to check a green run after the fact —
|
|
622
|
+
do not write throwaway scenarios just to get a picture.
|
|
623
|
+
|
|
612
624
|
## Failure modes you will hit
|
|
613
625
|
|
|
614
626
|
- **`StaleRefError`** — ref no longer in the DOM. Re-`get_page_snapshot`
|
|
@@ -735,11 +735,23 @@ the old browser context, then `run_test` again.
|
|
|
735
735
|
| `wait_for_url` | `value` (the pattern) | `options` |
|
|
736
736
|
| `enter_frame` | `locator` | |
|
|
737
737
|
| `exit_frame` | | |
|
|
738
|
+
| `screenshot` | | `name`, `fullPage`, `locator`, `outline`, `evidenceOnly` |
|
|
738
739
|
|
|
739
740
|
When recording (`explorationId` present): `description` + `section`
|
|
740
741
|
are required. Ad-hoc (no `explorationId`): no description/section,
|
|
741
742
|
no recording.
|
|
742
743
|
|
|
744
|
+
`screenshot` is the **evidence** step: it files a PNG under the session
|
|
745
|
+
(`.unotest/explorations/<explorationId>/screenshots/<NNN>-<name>.png`,
|
|
746
|
+
`adhoc/` without a session), replies with `{path, width, height}` and
|
|
747
|
+
the image itself, and becomes `screenshot(name)` in the saved test.
|
|
748
|
+
`name` defaults to the section slug; `locator` captures one element
|
|
749
|
+
(the saved test degrades that to a page capture); `outline: true` adds
|
|
750
|
+
the page outline to the reply; `evidenceOnly: true` keeps the file but
|
|
751
|
+
records no step. `explore_stop` lists every file in `artifacts`. Take
|
|
752
|
+
one wherever a human would want to check a green run after the fact —
|
|
753
|
+
do not write throwaway scenarios just to get a picture.
|
|
754
|
+
|
|
743
755
|
## Failure modes you will hit
|
|
744
756
|
|
|
745
757
|
- **`StaleRefError`** — a ref from an earlier lookup is no longer in
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,388 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.32.0] - 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 442fadf: `npx @unotest/web box …` reads a box's results from a terminal: `box envs` lists the environments a read token may look at, `box runs` their history (`--latest` collapses it to one line per scenario with its failing streak), `box run <id>` explains one run — the failure, the soft steps, the judge's verdicts — `box queue` shows who is waiting, and `box screenshot` saves a frame the run captured.
|
|
8
|
+
|
|
9
|
+
`box run --download` fetches the run's `*.unotest.zip` into `.unotest/box/` and unpacks its failure bundle into `.unotest/failures/`, where `list_failures`, `get_failure_*` and `agent_fix` already look — so a run that failed on a box is debugged with the commands a local failure is. `--no-screenshots` asks the box itself to leave the step frames out (`GET /api/runs/:id/export?screenshots=0`), which is what makes the download smaller rather than only the disk.
|
|
10
|
+
|
|
11
|
+
Reading needs a personal read token in `UNOTEST_BOX_READ_TOKEN`; the project's `UNOTEST_BOX_TOKEN` still pushes bundles and values and cannot read runs.
|
|
12
|
+
|
|
13
|
+
The viewer publishes its archive reader as `@unotest/viewer/snapshot`, so the three places that open a `*.unotest.zip` — its server, its browser bundle and now the CLI — share one implementation and one message for a file that is not an archive.
|
|
14
|
+
|
|
15
|
+
- 69fbea1: Six MCP tools close the loop that `bundle push --run` opens: `box_run` says what became of a run the agent ordered on a box, `box_run_download` unpacks its failure bundle into `.unotest/failures/` so `get_failure_trace`, `get_failure_console`, `get_failure_a11y`, `get_failure_screenshot`, `get_failure_network` and `agent_fix` work on it unchanged, and `box_envs`, `box_runs`, `box_queue` and `box_screenshot` cover the cases where the agent has no run id, no environment name, a run that never started, or a page it would rather see than read about. Their descriptions carry the route, not just the arguments.
|
|
16
|
+
|
|
17
|
+
Without a read token the tools refuse with a message saying where to mint one, so an agent meets an instruction rather than an unexplained failure.
|
|
18
|
+
|
|
19
|
+
- 9099f64: Box: personal read tokens, so an agent can read a box's runs without a browser.
|
|
20
|
+
|
|
21
|
+
A user mints a token for themselves on the guard's new **Read tokens** page
|
|
22
|
+
(`/_guard/tokens`), sees the value once, and points a client at the box with
|
|
23
|
+
`UNOTEST_BOX_READ_TOKEN`. The token is always `readonly` whatever its owner's
|
|
24
|
+
role, may only `GET`, and names the environment it means in
|
|
25
|
+
`X-Unotest-Environment: <project>/<environment>` (`GET /_guard/api/envs` lists
|
|
26
|
+
them). It is not a machine identity: it follows its owner — revoked, disabled
|
|
27
|
+
or a lapsed seat all stop it, and the refusal says which. An administrator
|
|
28
|
+
sees every token on the box and can revoke one that is not theirs; issuing,
|
|
29
|
+
first use and revocation all land in the audit trail.
|
|
30
|
+
|
|
31
|
+
`UNOTEST_BOX_TOKEN` is unchanged: the project token still pushes bundles and
|
|
32
|
+
environment values, and read tokens cannot — asking with the wrong one now
|
|
33
|
+
says which token the route wants instead of a bare "unauthenticated".
|
|
34
|
+
|
|
35
|
+
A read token is never passed on to the viewer behind the guard, so it cannot
|
|
36
|
+
end up in the logs of a service that has no use for it. A box whose licence
|
|
37
|
+
has lapsed, and an environment whose viewer is not up yet, answer a token in
|
|
38
|
+
the read contract's shape rather than with a page or a bare 503 — "wait" and
|
|
39
|
+
"renew the licence" are not the same instruction as "your token is wrong".
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- 5266b30: The agent integration guide's tool catalog matches the server again. It listed fifteen per-action tools (`goto`, `click`, `fill`, `press`, …) that were removed when recording moved into `explore_step`, had no section at all for the ten exploration tools that replaced them, counted the debugger's eleven tools as six, and put the total at 39 when it is 50. An agent reading it as a map — which is exactly what this file is for — would call tools that do not exist and conclude the server is broken. Every name now comes from the list the test suite verifies against the real registry, and the section counts add up to the total.
|
|
44
|
+
- 442fadf: `BoxReadClient` reads a box's runs — environments, run history, one run's whole journal, a collection's children, the queue, a run's export zip and its individual artifacts — over the same HTTP routes a browser uses, authenticated with the personal read token in `UNOTEST_BOX_READ_TOKEN`. Every refusal arrives as a typed `BoxReadError` whose `kind` says what to do next, so throttling is never mistaken for a rejected token and a live run's export says "wait" rather than looking like a broken box. Configuration mistakes surface when a read is attempted rather than at startup, so a stale token in a project's `.env` cannot stop a local run that never touches a box.
|
|
45
|
+
|
|
46
|
+
The viewer gains a `@unotest/viewer/wire` entry point exporting its HTTP contract types (the runs page, a run's full snapshot, the queue payload and the snapshot manifest), so a client can name the shapes it parses instead of keeping a second copy of them.
|
|
47
|
+
|
|
48
|
+
- f770a25: The box address and the read token are resolved from `unotest/.env` and `unotest/.secrets` on every request, not once at startup, and the `box` CLI reads those files directly. `unotest-web box …` runs without loading the project config, so nothing had flattened `unotest/.env` into its environment — the command told people to put the address there and then refused with "no box address". And an MCP server that resolved once meant a token written while it ran was ignored until the editor reconnected it, which is exactly the restart this path exists to avoid. One rule for both settings, from both entry points: a flag, then the environment, then the project's files.
|
|
49
|
+
- 8aa0f30: Protocol contract for reading a box's run results with a personal read token: `UNOTEST_BOX_READ_TOKEN` (distinct from the project's `UNOTEST_BOX_TOKEN`, which stays a write credential for bundles and environment values), the `X-Unotest-Environment: <project>/<environment>` header every bearer request names its own environment with, the `/_guard/api/envs` listing and its `BoxReadEnvironment` entry, and typed refusals (`unauthorized`, `forbidden`, `unknown-environment`, `not-found`, `rate-limited`, `run-in-progress`, `unavailable`, `malformed`) with parsers that reject anything that is not a box answering. Throttling gets a code of its own rather than an `unauthorized` carrying `Retry-After`: a rejected token means mint a new one, a throttled one means wait and resend the same one. The viewer's `ViewerEnvOption` is now that same protocol type rather than a second copy of it. An environment whose viewer is still starting — the common state right after a bundle push — answers `unavailable` rather than looking like an unreachable box, so the advice is to wait rather than to check the address.
|
|
50
|
+
- 8aa0f30: A run exported as `*.unotest.zip` now carries everything needed to diagnose it away from the machine that produced it: `stdout.log` and `stderr.log` (previously dropped, which left the one artifact that explains a runner crash outside the bundle), the failing page's `page.html` reachable through the manifest, and the run's step screenshots — declared in a new `manifest.screenshots` list, because an importer keeps only what the manifest names and a frame absent from it did not survive the round trip.
|
|
51
|
+
- 5266b30: `UNOTEST_BOX_READ_TOKEN` is now read from `unotest/.secrets` as well as from the environment, with an exported value winning. This is what makes the `box_*` tools usable from an agent at all: an MCP server is started by the editor, so a token that exists only in a shell is a token the server never receives — the alternative was editing the editor's own JSON config and reconnecting the server. `unotest/.env` is deliberately not consulted: it travels inside a pushed bundle, while `.secrets` does not. The CLI resolves the token the same way, so one token serves both.
|
|
52
|
+
- 5266b30: `box run` and `box_run` now carry the `{tag}` of a failed soft step, in the JSON and in the printed line (`Rubric [judge-red]`, the shape a local run already prints). A data-driven test runs one `step.soft` over many cases, so its failures all share a label and the tag is the only thing that says which case failed — without it a remote reader saw "Rubric failed" three times and had to tell them apart by line number. The tag was in the run's journal all along; the summary dropped it.
|
|
53
|
+
- ee6bb51: Secrets injected by a box are masked. Masking works by value, and the registry of values was built from `unotest/.secrets` alone — a file that does not exist on a box, where the daemon passes the values as environment variables and names them in `UNOTEST_BOX_SECRET_NAMES`. The registry was therefore empty on every box run, and a password a scenario typed reached the run journal, the run's `stdout.log`, the viewer's System pane and anything an agent downloaded, in the clear. The runner now registers those values alongside the ones it reads from files, so masking no longer depends on where the run happens to be. Nothing changes on a developer's machine, where the variable is not set.
|
|
54
|
+
- 047ca68: Failure text no longer carries terminal colouring into files and replies.
|
|
55
|
+
|
|
56
|
+
Playwright paints its call log whenever the environment claims a terminal is
|
|
57
|
+
watching — `FORCE_COLOR`, which an MCP server inherits from whatever launched
|
|
58
|
+
it — and that message was copied verbatim into `steps.jsonl`, `runtime.json`
|
|
59
|
+
and the failure bundle. An agent reading the JSON got an escape sequence in
|
|
60
|
+
the middle of the sentence it was trying to parse, and the viewer rendered
|
|
61
|
+
it as a chewed-up word.
|
|
62
|
+
|
|
63
|
+
The colouring is dropped where a thrown error becomes our data, so every
|
|
64
|
+
reader of a failure gets the same clean text. Our own output is unaffected:
|
|
65
|
+
it paints at print time, which is where colour belongs.
|
|
66
|
+
|
|
67
|
+
The same on mobile: a failure's text in the run journal and in the report no
|
|
68
|
+
longer carries terminal escapes.
|
|
69
|
+
|
|
70
|
+
- 047ca68: A failure somebody paused on stays a failure, with the evidence to show for it.
|
|
71
|
+
|
|
72
|
+
A run driven through the debugger — `run_test` then `resume`, or the viewer's
|
|
73
|
+
Continue — reported `completed` after a failure it had paused on, wrote no
|
|
74
|
+
failure bundle and no `failure/` artifacts, and left `list_failures` with
|
|
75
|
+
nothing to show. Continuing past a failure is how it gets inspected; it was
|
|
76
|
+
never meant to retract it. The verdict is now decided where the run's own
|
|
77
|
+
events are seen, so the journal, `runtime.json` and the reply agree, and a
|
|
78
|
+
debug run leaves the same evidence a plain `unotest-web e2e` run does.
|
|
79
|
+
|
|
80
|
+
`abort_runtime` (and Stop, and SIGTERM) now also ends the run in
|
|
81
|
+
`runtime.json`, not only in `steps.jsonl`: the control file used to keep
|
|
82
|
+
saying `paused-step` about a run that was over, so anything reading it rather
|
|
83
|
+
than the journal saw a pause that never ended.
|
|
84
|
+
|
|
85
|
+
- c5d3cda: README links the documentation site instead of manuals that are not shipped in the package (`guides/manuals/*`), and says what a run on a box cannot reach: `localhost`, port-forwards, host tools, a judge service on your machine — with the recipe for the judge on a box.
|
|
86
|
+
- 442fadf: Secret values registered from `unotest/.secrets` are now masked in what the runner prints, not only in what it writes. The run journal and every failure artifact were redacted; the terminal line was not, and that line is also copied into the run's `stdout.log` / `stderr.log` and streamed to the viewer's System pane. Masking is applied once, where the logger is built, so child loggers and message arguments are covered too. The collection runner is covered by the same rule: its messages go through its logger rather than straight to the stream, and the logger it builds when a caller passes none reads the project's secrets the way the composition root's does.
|
|
87
|
+
- 047ca68: A `step.soft(...)` failure no longer stops a debug run — or goes missing from it.
|
|
88
|
+
|
|
89
|
+
Under a debugger (`run_test`, or `e2e --debug`), every soft failure raised
|
|
90
|
+
the debug wheel: the run stopped on each one and an agent had to `resume`
|
|
91
|
+
its way through them. Worse, it was then lost — a paused failure is consumed
|
|
92
|
+
where it paused, so it never reached the `step.soft` envelope that records
|
|
93
|
+
it, the envelope closed as if the block had passed, and a run with three
|
|
94
|
+
soft failures could finish green with none of them listed.
|
|
95
|
+
|
|
96
|
+
Pausing is now for a failure that ENDS the run. A failure under any
|
|
97
|
+
enclosing soft step is recorded and stepped over exactly as it is on the
|
|
98
|
+
command line, and `runtime.json` — which the `run_test` reply is built from —
|
|
99
|
+
carries every soft failure of the run, not just the last stop.
|
|
100
|
+
|
|
101
|
+
A hard failure still pauses: that is what the debugger is for.
|
|
102
|
+
|
|
103
|
+
The same holds on mobile: a `step.soft(...)` failure no longer stops a run
|
|
104
|
+
under the debugger, and every soft failure of a run is now part of its
|
|
105
|
+
runtime state instead of being lost at the pause.
|
|
106
|
+
|
|
107
|
+
- Updated dependencies [442fadf]
|
|
108
|
+
- Updated dependencies [442fadf]
|
|
109
|
+
- Updated dependencies [8aa0f30]
|
|
110
|
+
- Updated dependencies [8aa0f30]
|
|
111
|
+
- Updated dependencies [047ca68]
|
|
112
|
+
- Updated dependencies [047ca68]
|
|
113
|
+
- Updated dependencies [047ca68]
|
|
114
|
+
- @unotest/viewer@0.32.0
|
|
115
|
+
- @unotest/protocol@0.32.0
|
|
116
|
+
- @unotest/dsl@0.32.0
|
|
117
|
+
- @unotest/core@0.32.0
|
|
118
|
+
- @unotest/grounder-client@0.32.0
|
|
119
|
+
|
|
120
|
+
## [0.31.0] - 2026-09-04
|
|
121
|
+
|
|
122
|
+
### Minor Changes
|
|
123
|
+
|
|
124
|
+
- b8e105b: New DSL function `note(label, value)`: attach a labelled value to the
|
|
125
|
+
current step — the question a data-driven case asked, the answer it got
|
|
126
|
+
— kept in the run journal and shown in the viewer under the step, during
|
|
127
|
+
and after the run. Any value: a string as it is, anything else as JSON;
|
|
128
|
+
cut at 4 KB (marked `truncated`), secrets masked. `log(...)` now also
|
|
129
|
+
lands in the journal under the step that wrote it (the stdout line
|
|
130
|
+
stays), and `assertJudge` verdicts carry the position of their statement,
|
|
131
|
+
so the viewer can show a verdict under its step and the whole verdict on
|
|
132
|
+
a failed step's error card. Each of these events records the DSL call's
|
|
133
|
+
`file` / `line` / `col` and the nearest entry-file statement
|
|
134
|
+
(`entryLine` / `entryCol`) — a note from inside a helper is attributed to
|
|
135
|
+
the entry step that called the helper. Without a run journal
|
|
136
|
+
(exploration, an ad-hoc runtime) `note` goes to the logger; nothing
|
|
137
|
+
fails.
|
|
138
|
+
- 56784b3: `ExecutionWalker.run` takes a trailing `entryArgs` list and binds it by
|
|
139
|
+
position onto the entry function's parameters (missing ones `null`) in
|
|
140
|
+
the root scope before the body runs — what `explore_run_flow` needs to
|
|
141
|
+
replay a parameterised `flow_*` helper live.
|
|
142
|
+
- 56784b3: `explore_run_flow` replays parameterised flows: pass `args` (positional
|
|
143
|
+
JSON values, `"{{NAME}}"` for a variable) and the helper's parameters are
|
|
144
|
+
bound for the live replay — `flow_login(username, password)` no longer
|
|
145
|
+
fails with `Variable "username" is not defined`. `explore_start` lists
|
|
146
|
+
each flow's `params`; a wrong argument count is refused with the
|
|
147
|
+
signature. The recording keeps variable NAMES, so the saved test reads
|
|
148
|
+
`flow_login(LOGIN, PASSWORD);` — `flow_call` is now a regular action
|
|
149
|
+
plugin rendering the call with its arguments.
|
|
150
|
+
- 307582f: New `explore_step` action `screenshot` — the evidence step of an
|
|
151
|
+
exploration. It files a PNG under the session
|
|
152
|
+
(`.unotest/explorations/<explorationId>/screenshots/<NNN>-<name>.png`,
|
|
153
|
+
`adhoc/` without a session), replies with the absolute `path`, `width`
|
|
154
|
+
and `height`, and returns the image itself as an MCP image block (a JPEG
|
|
155
|
+
copy when the PNG is over 1 MiB; path only when both are). `name`
|
|
156
|
+
defaults to the section slug, `fullPage` captures the whole page,
|
|
157
|
+
`locator` captures one element, `outline: true` adds the page outline
|
|
158
|
+
to the reply, `evidenceOnly: true` keeps the file but records no step.
|
|
159
|
+
A recorded screenshot becomes `screenshot(name)` in the saved test; an
|
|
160
|
+
element capture degrades to a page capture with a comment and a
|
|
161
|
+
non-blocking `DEGRADED_STEP` warning. `explore_stop` now lists the
|
|
162
|
+
session's files in `artifacts`. Driver: `DriverPage.screenshotElement`.
|
|
163
|
+
- 6b61884: New linter rule `lint:external-variable-undeclared` (default `error`): a
|
|
164
|
+
bare `UPPER_SNAKE` identifier that no layer file declares — `unotest/.env`,
|
|
165
|
+
`unotest/.secrets`, or the `.env.<name>` / `.secrets.<name>` overlay of
|
|
166
|
+
the environment `lint --env <name>` / `UNOTEST_ENV` selects — is reported
|
|
167
|
+
with the files searched, instead of failing at that statement at run time
|
|
168
|
+
after the browser is up. A name bound in the file (assigned, a parameter,
|
|
169
|
+
a loop counter) is never flagged. The shell is not a declaration: a value
|
|
170
|
+
CI exports still needs its name in `unotest/.env` (`NAME=` with no value
|
|
171
|
+
declares it; the shell's value wins at run time, as before). As an error
|
|
172
|
+
it is not silenced by `// lint-ok:`; downgrade it in `linter.rules` if a
|
|
173
|
+
project needs that.
|
|
174
|
+
|
|
175
|
+
`run_test` (MCP) and `e2e` run the same check before the browser starts,
|
|
176
|
+
against what the run will actually resolve — files and the ambient shell
|
|
177
|
+
— so a suite that lives on a shell variable keeps running; a name that
|
|
178
|
+
would have failed in the scenario anyway now fails up front (`run_test`
|
|
179
|
+
answers `lint_failed`; `e2e` prints it and proceeds, like every pre-run
|
|
180
|
+
diagnostic).
|
|
181
|
+
|
|
182
|
+
The linter also descends into operator, array and property-access
|
|
183
|
+
operands now, so a regex or `{{mustache}}` literal inside `a + b` is
|
|
184
|
+
diagnosed the same as one on its own.
|
|
185
|
+
|
|
186
|
+
- eddb1db: New linter rule `lint:one-test-per-file`: a second (third, …) top-level
|
|
187
|
+
`function test_*` in one scenario file is reported at its declaration.
|
|
188
|
+
A file is the unit a collection runs and the viewer shows — the Steps
|
|
189
|
+
tree projects a run onto the first `test_*`, so the others executed with
|
|
190
|
+
nowhere to be seen. Fold the cases into one test with tagged steps
|
|
191
|
+
(`step("…", {tag: id}, () => { … })`, `step.soft` when a case must not
|
|
192
|
+
stop the run) or move shared journeys into `flow_*` helpers under
|
|
193
|
+
`unotest/e2e/_helpers/`; `flow_*` functions in a scenario file are not
|
|
194
|
+
counted.
|
|
195
|
+
|
|
196
|
+
**Migration window:** the rule is a **warning** in 0.31 — visible in
|
|
197
|
+
`lint`, the editor and `run_test`'s warnings, silenced per line with
|
|
198
|
+
`// lint-ok: <reason>`, never blocking. A later minor release flips the
|
|
199
|
+
default to `error` (which the pre-run gate of `run_test` refuses); split
|
|
200
|
+
files that hold several `test_*` before then.
|
|
201
|
+
|
|
202
|
+
- b0a00cb: New DSL function `readJsonLine(path, filter) → object`: read a JSONL file
|
|
203
|
+
that already exists and return the first line matching the key filter —
|
|
204
|
+
no polling. `waitForJsonLine` was the only structured input into a
|
|
205
|
+
scenario, and for a fixture or a finished export its 20s timeout only
|
|
206
|
+
masked a wrong path. `readJsonLine` takes the same filter (strict equality
|
|
207
|
+
per key, dot paths for nesting, unparsable lines skipped) and fails at
|
|
208
|
+
once: `file not found: <path>` when the file is missing, `no line matches
|
|
209
|
+
{…} in <path>` with parse stats and the closest line when nothing matches.
|
|
210
|
+
Data-driven tests read their cases with it and name evidence after them
|
|
211
|
+
(`screenshot(q.id)`).
|
|
212
|
+
- 100ab9d: Data-driven steps: `step("label", {tag: q.id}, () => { … })` names the
|
|
213
|
+
case an iteration is on, and `step.soft("label", [{tag}], () => { … })`
|
|
214
|
+
records a failure inside instead of stopping the run — the rest of that
|
|
215
|
+
body is skipped, the next case runs, and the test still ends **failed**
|
|
216
|
+
with every soft failure listed (`3 soft step(s) failed: Question [q17]:
|
|
217
|
+
…`, one line per case in the CLI output; `run_test` answers
|
|
218
|
+
`next.softFailures`; `inspect_runtime`'s `lastFailure` carries `stepTag`
|
|
219
|
+
and `soft`). The failure bundle shows the page at the moment of the
|
|
220
|
+
**first** soft failure, with `soft: true` and `stepTag` in
|
|
221
|
+
`failure.json`; later soft failures live in the journal. `step.soft` is
|
|
222
|
+
allowed inside `test_*` only; nesting is free, and the outer step is a
|
|
223
|
+
group, not an assertion — a soft failure inside does not change its
|
|
224
|
+
outcome. The run journal gains `step-block:started` / `step-block:finished`
|
|
225
|
+
envelopes per block (label, tag, soft, outcome, whole-block duration).
|
|
226
|
+
Editor typings declare the new forms.
|
|
227
|
+
- ad7c918: The run journal records every loop pass (`loop:iteration`, with the loop
|
|
228
|
+
statement's position and the pass index), which the viewer's new Trace
|
|
229
|
+
view uses to show a loop as one group per iteration. Also: when an error
|
|
230
|
+
unwinds through enclosing statements, `lastFailure` (the failure bundle's
|
|
231
|
+
position, `inspect_runtime`, the CLI's `in step "…" [tag]` line) now
|
|
232
|
+
names the innermost statement that raised it — the assert inside the
|
|
233
|
+
tagged step — instead of the outermost loop that re-threw it.
|
|
234
|
+
|
|
235
|
+
### Patch Changes
|
|
236
|
+
|
|
237
|
+
- 9ae6d59: A bundle no longer declares the runner's own `UNOTEST_*` settings
|
|
238
|
+
(`UNOTEST_HEADED`, `UNOTEST_LOG_LEVEL`, …) from the suite's `.env`, so a box
|
|
239
|
+
stops warning on every push that an environment "neither sets nor holds"
|
|
240
|
+
names `env push` never sends. Both sides now share one rule
|
|
241
|
+
(`isRunnerSetting` in `@unotest/protocol`): those variables configure the
|
|
242
|
+
machine a run happens on, not the suite. A per-suite value such as the
|
|
243
|
+
navigation timeout travels with the bundle through `unotest.config.mjs`
|
|
244
|
+
(`defaultNavigationTimeoutMs`), not through `.env`.
|
|
245
|
+
- 917380d: `unotest.config.*` is merged with the defaults recursively. A partially
|
|
246
|
+
written section — `linter: { rules: { 'lint:deep-css': 'off' } }`,
|
|
247
|
+
`failureBundle: { tier3: { video: true } }`, `viewport: { width: 1920 }`
|
|
248
|
+
— now keeps the rest of that section's defaults instead of failing with
|
|
249
|
+
"config validation failed" (nested sections require all of their fields
|
|
250
|
+
at validation time, so a top-level replace made every partial section
|
|
251
|
+
invalid). Arrays and scalars still replace as a whole: `browsers:
|
|
252
|
+
['firefox']` means firefox only. The setup manual's example config,
|
|
253
|
+
which was exactly such a partial `linter`, loads as written; its
|
|
254
|
+
`timeouts` block — never a config field — is corrected to
|
|
255
|
+
`defaultTimeoutMs` / `defaultNavigationTimeoutMs`.
|
|
256
|
+
- e048334: The viewer's editor flags an undeclared external variable
|
|
257
|
+
(`lint:external-variable-undeclared`), the way `unotest-web lint` and the
|
|
258
|
+
pre-run lint already do: a bare `UPPER_SNAKE` identifier that no
|
|
259
|
+
`unotest/.env` / `.secrets` file of the active environment declares is an
|
|
260
|
+
error in the editor, naming the files searched. Protocol:
|
|
261
|
+
`DslValidateContext` gains `externalNames` / `externalSources`; the
|
|
262
|
+
viewer supplies them from the active target + environment's layer files
|
|
263
|
+
(the same files the variables panel shows) and `@unotest/web`'s language
|
|
264
|
+
service turns them into the linter's lookup.
|
|
265
|
+
- dcf4c9e: Viewer editor: lint markers appear as soon as a file opens (or the page
|
|
266
|
+
reloads), not only after the first keystroke — the editor validates the
|
|
267
|
+
source it mounted with instead of waiting for a change event that a
|
|
268
|
+
reopened tab never fires.
|
|
269
|
+
- 6646584: Docs: `hover` now lists its options (`force`, `position`, `timeout`) in
|
|
270
|
+
the DSL reference, the editor signature and the generated typings — it
|
|
271
|
+
used to read as if it took none, while `position` is exactly what a
|
|
272
|
+
hover-revealed menu at one edge of a tall element needs. The agent
|
|
273
|
+
integration guide gains "Watching a collection run": the collection's
|
|
274
|
+
`steps.jsonl` journal (path, the four `collection-run:*` events, how to
|
|
275
|
+
tail it) as the progress source for a run started in the background,
|
|
276
|
+
where the CLI's per-scenario lines only arrive when the process ends.
|
|
277
|
+
- 630d6ce: `unotest-web lint` now receives its arguments: the bin dispatcher forwarded
|
|
278
|
+
only the command name, so `lint --env <name>` linted the base environment
|
|
279
|
+
and an explicit file argument was ignored (every file was linted instead).
|
|
280
|
+
`UNOTEST_ENV=<name>` worked all along, which is why the gap went unnoticed.
|
|
281
|
+
- f3bd3cc: `lint` works on a project that has not migrated to the 0.28 layout yet.
|
|
282
|
+
A `unotest.config.*` still at the project root used to stop `lint` with
|
|
283
|
+
the same refusal `e2e` gives — right for a command that would RUN on
|
|
284
|
+
defaults, wrong for one that only grades: nobody lints a suite during
|
|
285
|
+
the very migration that touches every file. `lint` now builds its
|
|
286
|
+
context on the default settings (rule severities, helpers dir), reads
|
|
287
|
+
`unotest/.env`, `.secrets` and `_helpers/` as usual, and reports the
|
|
288
|
+
layout once as the new `lint:legacy-layout` warning (default `warn`,
|
|
289
|
+
configurable like any rule) with the migration on one line. The exit
|
|
290
|
+
code is unaffected by it. `e2e`, `collection` and `bundle push` refuse
|
|
291
|
+
exactly as before.
|
|
292
|
+
- 224d016: The MCP server says when it is running a stale build. `run_test`'s
|
|
293
|
+
pre-spawn lint, flow discovery (`explore_start`), `explore_run_flow`,
|
|
294
|
+
`generate_dsl_from_exploration` and `save_exploration_as_test` parse and
|
|
295
|
+
render DSL in the server's own process, on the modules it loaded at
|
|
296
|
+
start — after a rebuild a new DSL feature came back as a false parse
|
|
297
|
+
error until the server was restarted, while the runner child was already
|
|
298
|
+
on the new build. Those replies now carry `serverStale: true` and a
|
|
299
|
+
`serverStaleWarning` ("restart the MCP server") once the code on disk is
|
|
300
|
+
newer than the server's start; nothing changes while it is fresh.
|
|
301
|
+
- 02757c3: `serverStale` now also rides on `explore_step` / `explore_steps` (locator
|
|
302
|
+
resolution and recording run in the server's process), `find_element`
|
|
303
|
+
and `ground_element` replies — the same flag `run_test` and the
|
|
304
|
+
exploration tools already carry once the build on disk is newer than the
|
|
305
|
+
running MCP server.
|
|
306
|
+
- dcf4c9e: `goto` with a relative path resolves against the runtime's own base URL
|
|
307
|
+
instead of the browser context's. In a CLI run the two coincide; in an
|
|
308
|
+
exploration recording against an `env` override, `explore_run_flow`
|
|
309
|
+
replayed `goto('/_guard/')` on the MCP session's shared context — created
|
|
310
|
+
with the server's base — and landed on the wrong host. The replay now
|
|
311
|
+
follows the session's `baseUrl` (and its variables, as before).
|
|
312
|
+
- f3bd3cc: `screenshot(name)` accepts any string value, not only a literal. The
|
|
313
|
+
runtime always slugified whatever it received, but the validator (and
|
|
314
|
+
therefore `lint` and `run_test`) rejected `screenshot(q.id)` and
|
|
315
|
+
`screenshot(textJoin(['q-', id]))` — a data-driven test had to name every
|
|
316
|
+
capture the same. The `name` slot is now string-like, as in `fill`; a
|
|
317
|
+
non-string argument is still an `arg-kind` error.
|
|
318
|
+
- dcf4c9e: A section label reopened after another section is called out instead of
|
|
319
|
+
silently splitting a step: `explore_step` / `explore_steps` reply with
|
|
320
|
+
`sectionHint` the moment it happens, and the generated draft carries a
|
|
321
|
+
non-blocking `SPLIT_SECTION` warning naming the reopened entries. The
|
|
322
|
+
recorded order of actions is never changed; `autoRun` and `save` are not
|
|
323
|
+
held back by it.
|
|
324
|
+
- 9ae6d59: A scenario that finished in the same instant the viewer looked at it is no
|
|
325
|
+
longer recorded as `interrupted` with a finish an hour before its start.
|
|
326
|
+
The runner ends a run by appending `run:finished` and then backdating the
|
|
327
|
+
heartbeat; the viewer read those two files in the opposite order, so a
|
|
328
|
+
steps snapshot taken a few milliseconds early could meet the backdated
|
|
329
|
+
heartbeat, and the provisional verdict stuck in the run index while the
|
|
330
|
+
collection reported the scenario passed. The heartbeat is read first now.
|
|
331
|
+
A viewer restart (which rebuilds the index from disk) already corrected
|
|
332
|
+
such records; runs indexed live are right the first time.
|
|
333
|
+
- 13a2d51: Viewer, Home tab: the tile of a scenario that is running now breathes
|
|
334
|
+
through colour and shadow only — the scale pulse is gone. It moved the
|
|
335
|
+
tile's box every frame, so an automation driver never saw the tile as
|
|
336
|
+
stable and a click on it waited out its timeout. A tile that never ran
|
|
337
|
+
no longer carries `data-age="fresh"`: it has no age, so the attribute is
|
|
338
|
+
absent.
|
|
339
|
+
- 91fb562: Viewer, Home tab: clicking a scenario tile opens the scenario's LAST RUN —
|
|
340
|
+
the run whose verdict the tile's colour is showing — instead of the test
|
|
341
|
+
file. A tile that has never run still opens the test (there is no run to
|
|
342
|
+
open), and so does every tile in schedule mode, where the schedule is
|
|
343
|
+
edited from the test. The legend now reads "hover for history · click to
|
|
344
|
+
open the last run".
|
|
345
|
+
- dcf4c9e: Viewer, Home tab: the tile field no longer refits itself in a loop. The
|
|
346
|
+
header and legend take the grid's width; when the legend wrapped at one
|
|
347
|
+
width and not at the next, the field's height changed, the fit followed,
|
|
348
|
+
the width changed back — at 60 fps every tile's box moved every frame,
|
|
349
|
+
and an automation click on ANY tile ("element is not stable") waited out
|
|
350
|
+
its timeout unless forced. Header and legend now never wrap (they grow
|
|
351
|
+
past the grid instead), so the fit converges and tiles stay put. A run
|
|
352
|
+
opened from a tile is titled by its scenario, like one opened from the
|
|
353
|
+
Runs tree, instead of by its run id.
|
|
354
|
+
- dcf4c9e: Recording no longer bakes relative time or counters into a locator. An
|
|
355
|
+
accessible name such as `Run smoke — passed, 7m ago`, `just now`,
|
|
356
|
+
`yesterday`, `5 passed`, `7 runs` or a trailing badge (`Inbox · 3`,
|
|
357
|
+
`Errors: 12`) now counts as volatile: the recorder emits the stable prefix
|
|
358
|
+
(`{name: "Run smoke — passed", exact: true}`, then `/^Run smoke — passed\b/`)
|
|
359
|
+
instead of the literal, and the draft carries the `DYNAMIC_TEXT` warning
|
|
360
|
+
with that prefix. Bare numbers elsewhere stay stable (`Page 2`, `Q4 2026`,
|
|
361
|
+
`v2`, `2FA`).
|
|
362
|
+
- Updated dependencies [b8e105b]
|
|
363
|
+
- Updated dependencies [b8e105b]
|
|
364
|
+
- Updated dependencies [b8e105b]
|
|
365
|
+
- Updated dependencies [9ae6d59]
|
|
366
|
+
- Updated dependencies [56784b3]
|
|
367
|
+
- Updated dependencies [e048334]
|
|
368
|
+
- Updated dependencies [dcf4c9e]
|
|
369
|
+
- Updated dependencies [100ab9d]
|
|
370
|
+
- Updated dependencies [100ab9d]
|
|
371
|
+
- Updated dependencies [100ab9d]
|
|
372
|
+
- Updated dependencies [100ab9d]
|
|
373
|
+
- Updated dependencies [ad7c918]
|
|
374
|
+
- Updated dependencies [ad7c918]
|
|
375
|
+
- Updated dependencies [ad7c918]
|
|
376
|
+
- Updated dependencies [9ae6d59]
|
|
377
|
+
- Updated dependencies [13a2d51]
|
|
378
|
+
- Updated dependencies [91fb562]
|
|
379
|
+
- Updated dependencies [dcf4c9e]
|
|
380
|
+
- @unotest/core@0.31.0
|
|
381
|
+
- @unotest/protocol@0.31.0
|
|
382
|
+
- @unotest/viewer@0.31.0
|
|
383
|
+
- @unotest/dsl@0.31.0
|
|
384
|
+
- @unotest/grounder-client@0.31.0
|
|
385
|
+
|
|
3
386
|
## [0.30.0] - 2026-09-03
|
|
4
387
|
|
|
5
388
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -7,6 +7,11 @@ and commit.**
|
|
|
7
7
|
MCP server + CLI runner + JavaScript DSL + semantic DOM snapshots +
|
|
8
8
|
structured failure bundles.
|
|
9
9
|
|
|
10
|
+
Full documentation — manuals, the box, CI, the judge — lives at
|
|
11
|
+
[docs.unotest.com](https://docs.unotest.com). This README is the short
|
|
12
|
+
tour; the package ships only `guides/dsl-reference.md` and
|
|
13
|
+
`guides/agent-integration.md` alongside it.
|
|
14
|
+
|
|
10
15
|
---
|
|
11
16
|
|
|
12
17
|
## 1. Setup
|
|
@@ -170,7 +175,7 @@ a suite takes minutes, and a request held open that long is a timeout,
|
|
|
170
175
|
not a result. Watch them in the viewer, or let the box report the checks
|
|
171
176
|
back to GitHub. `--pr` makes a newer push withdraw the older runs of the
|
|
172
177
|
same pull request that are still waiting, so three pushes in five minutes
|
|
173
|
-
cost one suite. Recipe:
|
|
178
|
+
cost one suite. Recipe: [Pushing suites → A minimal CI job](https://docs.unotest.com/box/pushing-suites/#a-minimal-ci-job).
|
|
174
179
|
|
|
175
180
|
The values a suite runs with on the box — `APP_BASE_URL`, its `.env`
|
|
176
181
|
settings, its secrets — are sent separately, from the same files a local
|
|
@@ -184,7 +189,66 @@ printf '%s' "$KEY" | npx @unotest/web env set dev API_KEY --secret
|
|
|
184
189
|
`.env*` become the environment's variables, `.secrets*` its secrets,
|
|
185
190
|
`APP_BASE_URL` its target; `UNOTEST_*` stay home. A value never prints.
|
|
186
191
|
The token must be minted with `--values` on the box. Admins also see and
|
|
187
|
-
change them on the box's admin page. Manual:
|
|
192
|
+
change them on the box's admin page. Manual: [An environment's values on a
|
|
193
|
+
box](https://docs.unotest.com/box/environments-and-values/).
|
|
194
|
+
|
|
195
|
+
A run on the box happens inside the box's own container: nothing on your
|
|
196
|
+
machine is reachable from it — no `localhost`, no `kubectl port-forward`,
|
|
197
|
+
no tools installed on your laptop. Preconditions must probe the
|
|
198
|
+
environment's public URL, and `assertJudge` runs its judge in-process there
|
|
199
|
+
(`UNOTEST_JUDGE_MODE=local`, `@unotest/judge` in the suite's
|
|
200
|
+
`package.json`, the API key as a secret). Details:
|
|
201
|
+
[Judge on a box](https://docs.unotest.com/guides/judge/#on-a-box) and
|
|
202
|
+
[What a box cannot reach](https://docs.unotest.com/box/troubleshooting/#what-a-box-cannot-reach).
|
|
203
|
+
|
|
204
|
+
### Reading results from a box
|
|
205
|
+
|
|
206
|
+
`bundle push --run` gives you a run id. Read what became of it from the
|
|
207
|
+
same terminal — no browser, no shell on the box:
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
npx @unotest/web box runs --env acme/test --latest
|
|
211
|
+
npx @unotest/web box run checkout-mqf3pwr1 --env acme/test
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`box run` prints the failure, the results of soft steps and the judge's
|
|
215
|
+
verdicts. When you want the artifacts too:
|
|
216
|
+
|
|
217
|
+
```sh
|
|
218
|
+
npx @unotest/web box run checkout-mqf3pwr1 --env acme/test --download
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
That saves the run's `*.unotest.zip` under `.unotest/box/` and unpacks its
|
|
222
|
+
failure bundle into `.unotest/failures/` — where `list_failures`,
|
|
223
|
+
`get_failure_*` and `agent_fix` already look, so your agent debugs a box
|
|
224
|
+
failure with the commands it uses for a local one. Add `--no-screenshots`
|
|
225
|
+
to leave the step frames on the box; they are usually most of the bytes.
|
|
226
|
+
|
|
227
|
+
Reading uses a **personal** read token, not the project one. Put it in
|
|
228
|
+
`unotest/.secrets`, next to your other project secrets:
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
UNOTEST_BOX_READ_TOKEN=ubr_...
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
and the box's address in `unotest/.env` as `UNOTEST_BOX_URL`. Both files
|
|
235
|
+
are read directly, so this works the same from the terminal and from your
|
|
236
|
+
agent. Exporting either value in your shell works too and wins over the
|
|
237
|
+
file; `--box` and `--token` win over both.
|
|
238
|
+
|
|
239
|
+
The token is never read from `unotest/.env` — that file travels inside a
|
|
240
|
+
pushed bundle, and `.secrets` does not.
|
|
241
|
+
|
|
242
|
+
Keeping it in the file is what lets your agent read runs: an MCP server is
|
|
243
|
+
started by your editor, so a token that only exists in your shell is a
|
|
244
|
+
token the agent never sees. A token added while the server is running is
|
|
245
|
+
picked up on the next call — nothing to restart.
|
|
246
|
+
|
|
247
|
+
You mint it for yourself on the box's guard ("Read tokens"), it is shown
|
|
248
|
+
once, and it is revocable. It is read-only whatever your role on the box — it cannot start
|
|
249
|
+
a run or change a value. `UNOTEST_BOX_TOKEN` is the project's push
|
|
250
|
+
credential and cannot read runs. `box envs` lists the environments a token
|
|
251
|
+
may look at.
|
|
188
252
|
|
|
189
253
|
## 5. Watch it run — the viewer
|
|
190
254
|
|
package/bin/unotest-web.js
CHANGED
|
@@ -75,7 +75,9 @@ function dispatch(sub) {
|
|
|
75
75
|
forwardArgs: args.slice(1),
|
|
76
76
|
};
|
|
77
77
|
case "lint":
|
|
78
|
-
|
|
78
|
+
// Forwarded whole, like e2e: `--env <name>` and explicit file
|
|
79
|
+
// arguments belong to the command, not to the dispatcher.
|
|
80
|
+
return { dist: "dist/runner/cli.js", src: "src/runner/cli.ts", forwardArgs: args };
|
|
79
81
|
case "viewer":
|
|
80
82
|
return { dist: "dist/runner/cli.js", src: "src/runner/cli.ts", forwardArgs: ["viewer"] };
|
|
81
83
|
case "prepare-fix":
|