@unotest/web 0.45.0 → 0.47.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/CHANGELOG.md +83 -3
- package/README.md +7 -0
- package/dist/config/schema.d.ts +260 -73
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +10 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +24 -2
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +10 -2
- package/dist/inspection/page-inject.js +12 -0
- package/dist/{interfaces-OvnJKELk.d.ts → interfaces-LE6GsJ0c.d.ts} +64 -3
- package/dist/mcp/server.js +1 -1
- 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/dsl-reference.md +416 -17
- package/package.json +9 -8
- package/types/unotest-dsl.d.ts +122 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.47.0] - 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2e847ae: New DSL getter `computedStyle(locator, property)`: the element's computed CSS value as the browser resolved it — `computedStyle(card, "opacity")` → `"0.5"`, custom properties too (`"--accent"`), `""` when the property is not set. A missing or ambiguous element fails as `getAttribute` does. Replaces reading styles through `evaluate`.
|
|
8
|
+
- 3c558c8: New DSL call `emulateMedia({ reducedMotion?, colorScheme? })` changes the media features the current tab reports mid-scenario — `prefers-reduced-motion` (`'reduce'` / `'no-preference'`) and `prefers-color-scheme` (`'light'` / `'dark'` / `'no-preference'`), as `matchMedia` and CSS `@media` see them. It applies to the current tab only; a tab or popup opened later starts without it. Replaces overriding `matchMedia` through `evaluate`.
|
|
9
|
+
- aa2c57f: New DSL functions for numbers, so a scenario no longer shells out to `node -e` to compare one. `toNumber(text, {decimal?})` reads a number as a page writes it: spaces and NBSP group digits, `1,234.56` and `1.234,56` both read as 1234.56, and an ambiguous `1,234` fails the step naming both readings until `{decimal}` settles it; units and currency fail, quoting the input. `round(n, digits?)` rounds half away from zero on the decimal as written, so `round(1.005, 2)` is 1.01. `formatNumber(n, {locale?, minFractionDigits?, maxFractionDigits?})` formats through `Intl.NumberFormat`, en-US unless a locale is given, whatever the machine's language. The lint answer to `parseInt`, `parseFloat`, `Number`, `toFixed` and `toLocaleString` now points at these functions.
|
|
10
|
+
- 832e6de: Quoted DSL strings now decode the ES5 escapes `\r`, `\f`, `\v`, `\0`, `\xHH` and `\uXXXX`. Behaviour change: `'\u00A0'` used to be six characters and is now one non-breaking space, and a malformed `\u12` or `\xG1` is a parse error instead of being kept as text. In a string a regex is built from, `\x2e` / `\u002e` now give the character itself — a `.` that matches anything — not an escaped one; for a literal dot write `\\.`. Any other backslash pair stays as written, `\b` included (a word boundary in a regex string, never a backspace), so `'\d+'` and `'\bword\b'` work as before; backtick strings remain raw.
|
|
11
|
+
- a89bf5e: New DSL calls for CSS animations: `waitForAnimations(locator, { timeout? })` waits until the element and its descendants have no animation still running (default 5000 ms; a timeout names the element and how many were left), and `hasAnimation(locator)` says whether one is running. Infinite animations such as spinners, and paused ones, are not waited for. Replaces waiting on `getAnimations()` through `evaluate`.
|
|
12
|
+
- 783151c: An environment on a box that somebody pointed at a bundle by hand can follow its branch again. Selecting a bundle in the viewer holds the environment on it, and later pushes to the branch did not arrive; nothing showed why, and nothing let go short of a restart. The bundle menu now says `pinned by <name>` and offers "follow <branch> again", which moves the environment to the branch's newest bundle and keeps it following. The viewer's workspace file gains an optional `follow` field (`path`, `branch`), present only while such a selection holds; older viewers ignore it, and an older box never writes it.
|
|
13
|
+
- b16e326: A scenario that calls `assertJudge` now checks its judge before the browser starts, instead of failing at the first verdict after the steps before it had already run. The check follows calls into helpers, and a scenario that never judges is not held up by a broken judge setup. `e2e` refuses with exit code 78 and the message that names what to fix: the mode is unset, a provider variable is missing in `local` mode, or in `remote` mode the service does not answer its `/health` (start one with `npx @unotest/judge`). A collection asks once, before any scenario starts. MCP `run_test` answers such a refusal with the new `status: "config_error"` rather than `spawn_failed`, so an agent fixes the setup, not the launch. The viewer's environment check gains an "LLM judge (assertJudge)" line that asks the same question.
|
|
14
|
+
- 0f1fca5: `bundle push` names the project the bundle landed in: `✓ uploaded to <box> · project <name>`. The token decides the project, not the repository, so when the box maps the bundle's repository to no project the push still lands and is followed by a warning that says where it went and on which token; the box logs the same warning. `--json` carries `project`, `tokenLabel` and, when it applies, `unmappedRepo`. A box older than these fields gets the previous line, without the project.
|
|
15
|
+
- af57556: `bundle push --run` prints the id of every run it queued and, when the box reports one, the run's link in the box's viewer. `--json` carries the link as `runs[].url`. A box that does not know the address it is reached at, or one older than this field, sends no link: the id is printed alone.
|
|
16
|
+
- 03e37b8: `bundle push --run --follow` waits for the runs it ordered and exits with their verdict: `0` when every suite passed, `1` when one failed, `2` when there is no verdict to give (a box too old to be followed, a box that restarted and forgot the run, or an hour without an ending), each with its reason and the run's link. The box shows a run's state only to the push token that ordered it, so a CI job needs no read token and no session. `--json` adds `follow`. The box's guard lets a run's state through on the project token without a session for `GET` only; every other method on that path still needs a session.
|
|
17
|
+
- 0c1d214: `assertUrl` and `waitForUrl` take a `/regex/` as well as a string. A regex is tested against the whole URL, unanchored, so `assertUrl(/\/cars\/\d+$/)` checks a path with an id in it; a failed `assertUrl` names the pattern as `/source/flags`. A string is still a substring match, so existing scenarios behave as before. The documented `{timeout}` option of both is no longer flagged as an extra argument by the editor and `lint`.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 1a159a8: A collection whose `webServer` sets `reuseExistingServer: false` no longer fails every scenario with "already serving". The collection starts the server once for the whole suite, its scenarios run on that server, and it is stopped after the last one. A port held by another process still fails the collection before its first scenario.
|
|
22
|
+
- b374e1d: `readFile`, `readFileInfo`, `readJson`, `upload()` and the file waits (`waitForFile`, `readJsonLine`, `waitForJsonLine` and the rest) now read the file they checked. A process writing in the project could swap a file for a symlink out of it between the check and the read, and the scenario got the outside file. The file is opened once and read from that handle only when it is the file the path leads to; a one-off read that meets a file changing under it fails saying so, a wait reads it again on its next tick.
|
|
23
|
+
- d7ab07d: The "follow again" row in the viewer's bundle menu now shows its explanation in full: `pinned by <name> — the next push to <branch> will not arrive until then` wraps onto a second line instead of being cut off with an ellipsis. The bundle rows above it are unchanged.
|
|
24
|
+
- d33d8c0: The `lint:evaluate-discouraged` warning now names the typed calls that replace the common `evaluate` uses — `computedStyle` for styles, `boundingBox` for geometry, `hasAnimation` / `waitForAnimations` for animations — next to the attribute and text getters.
|
|
25
|
+
- e924fec: A tool error from an MCP server that started before its code was last built now says so. Right after a package update, `run_test` on a scenario using a new DSL feature answered a bare parse error from the old modules still loaded in the server; now any tool error on such a server comes back as JSON `{error, serverStale: true, serverStaleWarning}` — the original message plus the advice to restart the MCP server. On a server that is up to date, errors read exactly as before.
|
|
26
|
+
- b9e6cd5: An MCP server closed right after it started no longer leaves a browser running. The server starts its browser in the background as soon as the client connects; a client that closed within those first seconds made the server exit mid-launch, and the browser came up afterwards with nothing left to close it. The server now waits for that launch before it shuts down.
|
|
27
|
+
- ada5785: `mockRoute('**/*', {abort: true})` no longer kills a browser extension's own pages and scripts. A mask reaches web requests only (http, https, ws, wss), so an extension panel keeps loading under a network allowlist instead of going dead with nothing in the console. To mock an extension resource, start the mask with its scheme, as in `chrome-extension://<id>/panel.js`; such a mask still answers even when a `**/*` mock was added after it.
|
|
28
|
+
- 6f872c9: A link to a collection run now opens that run even when it is older than the first page of the viewer's history. Such links — from `bundle push --run`, CI statuses and notifications — opened the run's tab and then showed "not found". The tab now shows the collection's scenarios as for a recent run, says "loading run…" while it is fetched, and reports "not found" only when the run really does not exist.
|
|
29
|
+
- 87487b2: A link to a run on a box — in a CI check, a run notification or the output of `bundle push --run` — now opens that run in the box's viewer. Links have the form `<box>/_guard/runs/<project>/<environment>/<run id>`; following one without a session signs in first and comes back to the same run. The viewer opens a run named by `?run=<id>` in its own tab, and ignores an id it does not have.
|
|
30
|
+
- f07e83f: A run on a box gets `TMPDIR=/tmp` beside `HOME=/tmp`, and an environment's value of the same name does not replace it. Node already fell back to `/tmp`; a shell script or another tool in a `shell()` step did not, and wrote into the read-only sources instead.
|
|
31
|
+
- ff19ac3: Switching an environment to another bundle from the viewer now needs an `Origin`, like ordering a run and following the branch again. A request under a session cookie that carries no `Origin` is refused with 403 `origin required` before it reaches the box daemon; the viewer's own bundle menu sends one and works as before.
|
|
32
|
+
- cccd35c: Messages about a remote judge no longer print the user, password or query of `UNOTEST_JUDGE_URL` — the places where gateways put keys. The check before the first step, the MCP `config_error` reason, a refused collection and a failed `assertJudge` name the service by scheme, host and path only; requests still go to the URL as given. A URL with a user or password in it, which fetch never sent, is now refused up front with a pointer to `UNOTEST_JUDGE_TOKEN`, and a value that is not a URL is refused without being repeated.
|
|
33
|
+
- 2fb7385: The viewer rebuilds its run history index again on its own after a failure, instead of waiting for a restart. A rebuild the disk refuses (permissions on the runs folder, a volume mounted late) is retried with a growing pause, from 5 seconds up to 5 minutes, until it succeeds. `/api/health` still answers 200 and now carries `runIndex`: `building`, `built`, or `failed` with the error code, the path inside the project, the number of attempts and the time of the last one.
|
|
34
|
+
- Updated dependencies [832e6de]
|
|
35
|
+
- Updated dependencies [783151c]
|
|
36
|
+
- Updated dependencies [d7ab07d]
|
|
37
|
+
- Updated dependencies [6f872c9]
|
|
38
|
+
- Updated dependencies [0f1fca5]
|
|
39
|
+
- Updated dependencies [af57556]
|
|
40
|
+
- Updated dependencies [03e37b8]
|
|
41
|
+
- Updated dependencies [87487b2]
|
|
42
|
+
- Updated dependencies [2fb7385]
|
|
43
|
+
- @unotest/dsl@0.47.0
|
|
44
|
+
- @unotest/viewer@0.47.0
|
|
45
|
+
- @unotest/protocol@0.47.0
|
|
46
|
+
- @unotest/core@0.47.0
|
|
47
|
+
- @unotest/grounder-client@0.47.0
|
|
48
|
+
|
|
49
|
+
## [0.46.0] - 2026-10-02
|
|
50
|
+
|
|
51
|
+
### Minor Changes
|
|
52
|
+
|
|
53
|
+
- 5a047cc: Assertions take a regex and can wait for the opposite. `assertText(loc, expected, {not})` accepts a `/regex/` as well as a string, and `{not: true}` passes once the text stops matching, for a spinner label that has to go away. New `assertTexts(loc, [...])` checks a whole list in order, with the element count, and `assertAttribute(loc, name, expected, {not})` waits for an attribute value such as `aria-expanded`. A string still compares whole by default, `{exact: false}` for a substring, so existing scenarios behave as before. The "From @playwright/test" page maps `toHaveText`, `not.toHaveText` and `toHaveAttribute`.
|
|
54
|
+
- b6b4558: New DSL function `assertSchema(value, schema)`: checks a value against a JSON Schema (draft 2020-12), such as one your app generates from its zod schemas with `z.toJSONSchema` and the scenario reads with `readJson`. When the value breaks the schema, the step fails naming every broken field by path with what is wrong there (`/tags/1: must be string`), not only the first one. A `$ref` inside the schema (`#/$defs/...`) is followed; a `$ref` to another file is refused, as is a schema that declares another draft or is itself invalid. `format` is not checked. `@unotest/web` now depends on `ajv`.
|
|
55
|
+
- d9bb5af: The page console in scenarios: `consoleEntries({level?, since?})` returns what the active page logged as `[{level, text, url}]`, `pageErrors({since?})` the errors it threw that nothing caught as `[{message, stack}]`, and `assertNoConsoleErrors({includePageErrors?, since?})` fails on either, quoting the first three. Pages are recorded from the moment they open, extension pages (`chrome-extension://`) included, so a scenario acts first and asks afterwards; `since` takes a time from `nowMs()`. `UNOTEST_CONSOLE` still decides alone whether a run writes `console.json` into its artifacts and failure bundles.
|
|
56
|
+
- 8241cfb: New DSL function `env(name)`: returns an environment value as a string, or `null` when the name is not set, so a test can decide on it (skip a live-only check, pick a target) instead of failing on a missing variable. It reads what a bare `UPPER_SNAKE` name reads: the environment the run started in, then `unotest/.env`. A secret, from `unotest/.secrets` or injected by a box, is refused: write its bare name where the value is typed, and it stays masked.
|
|
57
|
+
- 25702f0: A new browser profile for every test: `launch: { profile: 'per-test' }` starts each `test_*` in a new browser on a new, empty profile directory, so an extension loads again, its service worker starts from scratch and nothing in `chrome.storage`, `localStorage` or the tabs carries over; with one test per file, a suite of N files runs on N profiles. The directory is removed after a passing test and kept in the run folder after a failing one; the run log names it. Browser extensions and the service-worker functions (`waitForServiceWorker`, `evaluateInServiceWorker`, `extensionId`) now work on per-test profiles too, not only with `launch.userDataDir`, and `mockRoute` answers the worker's own requests in every test. It cannot be combined with `launch.userDataDir`, and it is refused with `storageState`, with more than one worker and on a box; `UNOTEST_USER_DATA_DIR` overrides it with one shared profile. An agent joined to a debug run follows it into each test's browser.
|
|
58
|
+
- 8241cfb: New DSL function `skip(reason, condition?)`: a test ends itself as skipped when it does not apply here, for example `skip('no live API configured', env('LIVE_API') == null)`. Without a condition it always skips; with a false one the test goes on. `skip` is a test guard: it stands in the `test_*` body next to the steps, like `step(...)` and `teardown(...)`, and the validator accepts it there. The run reports skipped with the reason, neither passed nor failed: the CLI prints `○ test: … skipped: <reason>` and counts `1 skipped`, a collection counts `skipped=N(test=N)`, and `run_test` answers `outcome: "skipped"` with `skipReason`. The viewer does not know skipped yet: it shows such a run as passed, without the reason. The exit code stays 0. Nothing after the call runs; teardown blocks registered before it still play. A skip after a failed soft step, or a skipped test whose teardown fails, is a failed test. A new lint rule, `lint:skip-outside-test` (error), reports `skip()` anywhere else: inside a step or a block, in a helper or a `flow_*`. Validator contracts gain an optional `testGuard` flag: a function marked with it may stand as a direct child of a `test_*` body. On the wire a skipped run is `completed` with a new optional `skipReason` field on `run:finished` and in the runtime state, so readers that predate it still see a finished run.
|
|
59
|
+
- 0fa0343: A test's time budget, set where you expect it. The config key `testTimeoutMs` (default 300000) is the deadline of one test: an action still running when it runs out is cut there, the test fails naming `testTimeoutMs`, and its teardown blocks still play. `30000` gives every test 30 s. The new DSL function `setTestTimeout(ms)`, the counterpart of Playwright's `test.setTimeout`, gives one test its own budget, longer or shorter, counted from the test's start; the error then names `setTestTimeout(ms)`. `UNOTEST_MAX_DURATION_MS` still overrides the config for a run and keeps its name in the error. A new lint rule, `lint:set-test-timeout-outside-test` (error), reports `setTestTimeout()` outside a `test_*` body. Running out of time fails the test; Stop in the viewer still aborts the run.
|
|
60
|
+
- 3f18c02: Actions (`click`, `doubleClick`, `fill`, `press`, `check`, `uncheck`, `hover`, `selectOption`, `dragAndDrop`), navigation (`goto`, `reload`, `goBack`, `goForward`, `waitForUrl`, `waitForNavigation`) waits (`waitFor`, `waitForText`, `waitForCount`) and assertions (`assertText`, `assertTexts`, `assertAttribute`, `assertVisible`, `assertHidden`, `assertValue`, `assertCount`, `assertUrl`, `assertNeverAppears`) now refuse an option they do not take, naming the ones they do: `click(btn, {timout: 5000})` used to wait the default 3 s without a word.
|
|
61
|
+
|
|
62
|
+
**Migration:** a misspelled option in these functions now fails naming the supported ones — fix the spelling the message points at. `noWaitAfter` on `click` and `fill` never had an effect; it is now refused, so remove it.
|
|
63
|
+
|
|
64
|
+
- fefe1e7: The file oracles now read only where `readFile` does. `waitForFile`, `assertNoFile`, `waitForJsonLine`, `readJsonLine`, `assertNoJsonLine`, `waitForFileCount` and `assertFileCount` take a path relative to `sandbox.shellCwd`, or an absolute one inside the project, the run's downloads or a directory listed in the new `sandbox.readRoots`. Anything else (`../` out of them, an absolute path elsewhere, a symlink that leads out) fails the step with a scenario error naming the path and the directories it left. The check repeats on every read of a wait, so a file that appears as a symlink out is refused when it appears.
|
|
65
|
+
|
|
66
|
+
`readFile` and `readFileInfo` are stricter too: a file that does not exist yet is judged by the nearest directory above it that does, so a path under a directory symlinked out of the project is refused instead of reported missing. They also read from `sandbox.readRoots`.
|
|
67
|
+
|
|
68
|
+
Migration: a scenario that waits on or reads a file by an absolute path outside the project, such as a background listener's log in `/tmp`, now fails with that error. List the directory in the config: `sandbox: { readRoots: ['/tmp/my-listener'] }`.
|
|
69
|
+
|
|
70
|
+
- bb538e7: `webServer` in the config takes one server or a list — a front and the fake backends it talks to, each with its own readiness `url`. A list starts all at once, the first scenario runs once every `url` answers, and the servers stop in reverse order; if one does not come up, the ones that did are stopped and the error names each that failed. Two entries on one `url` are refused. `app-server` prints one event per server. Type change: `UnotestConfig.webServer` is now always a list once the config is parsed (an object in the file still works).
|
|
71
|
+
|
|
72
|
+
### Patch Changes
|
|
73
|
+
|
|
74
|
+
- cd4664d: Stopping a `webServer` the run started now waits until the server itself has exited, not only the shell that launched it. On Linux the shell exits at once on the stop signal while the server is still shutting down, so the next step could find its port still taken or its shutdown output unwritten.
|
|
75
|
+
- Updated dependencies [8241cfb]
|
|
76
|
+
- Updated dependencies [0fa0343]
|
|
77
|
+
- @unotest/dsl@0.46.0
|
|
78
|
+
- @unotest/protocol@0.46.0
|
|
79
|
+
- @unotest/viewer@0.46.0
|
|
80
|
+
- @unotest/core@0.46.0
|
|
81
|
+
- @unotest/grounder-client@0.46.0
|
|
82
|
+
|
|
3
83
|
## [0.45.0] - 2026-10-01
|
|
4
84
|
|
|
5
85
|
### Minor Changes
|
|
@@ -1490,7 +1570,7 @@ picker on` fetches this project's own access and writes the three grounder
|
|
|
1490
1570
|
|
|
1491
1571
|
That screen named one command:
|
|
1492
1572
|
|
|
1493
|
-
|
|
1573
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
1494
1574
|
|
|
1495
1575
|
In a project that has just been created, the command refuses with `no box
|
|
1496
1576
|
address` before it does anything: the project holds neither the box's address
|
|
@@ -1499,8 +1579,8 @@ address` before it does anything: the project holds neither the box's address
|
|
|
1499
1579
|
|
|
1500
1580
|
The screen now names both steps, in the order they have to happen:
|
|
1501
1581
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1582
|
+
npx @unotest/web box access larch
|
|
1583
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
1504
1584
|
|
|
1505
1585
|
The box's own name is filled in when its licence carries one. `box access`
|
|
1506
1586
|
with no name refuses, and its refusal offers to create a box — to somebody who
|
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ Testing a browser extension? `launch` in `unotest.config` starts the browser
|
|
|
38
38
|
with a profile (`userDataDir`) and extra arguments (`args`, e.g.
|
|
39
39
|
`--load-extension=./ext`; Chromium only). On a profile every scenario shares
|
|
40
40
|
one browser context — no isolation between them, and the run log says so.
|
|
41
|
+
`profile: 'per-test'` instead gives every test a new browser on a new,
|
|
42
|
+
empty profile: a suite of N files runs on N profiles.
|
|
41
43
|
|
|
42
44
|
## 2. Open the viewer
|
|
43
45
|
|
|
@@ -162,6 +164,11 @@ one of them, and `box logs` the end of a box's own log without logging in to
|
|
|
162
164
|
it; `box destroy` does what it says, and `whoami` and `logout` complete the
|
|
163
165
|
set. In CI, export `UNOTEST_CLOUD_TOKEN` instead of signing in.
|
|
164
166
|
|
|
167
|
+
On a box the suite's files are read-only and a run may write in two places
|
|
168
|
+
only. Which ones, the variables a run is given and where a `shell()` step
|
|
169
|
+
starts are on one page:
|
|
170
|
+
[What a run gets](https://docs.unotest.com/box/what-a-run-gets/).
|
|
171
|
+
|
|
165
172
|
Credit is added in the cabinet, by card, under "Add credit" on its home page
|
|
166
173
|
— never from the terminal. When the balance does not cover a new box's first
|
|
167
174
|
hour, `box create` says how much is missing (as far as the key may read the
|