@unotest/web 0.44.0 → 0.46.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 +73 -3
- package/README.md +2 -0
- package/dist/config/schema.d.ts +385 -46
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +12 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +37 -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 +1 -1
- package/dist/{interfaces-DEmGI704.d.ts → interfaces-CYoL4Taq.d.ts} +189 -6
- 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 +653 -5
- package/package.json +9 -8
- package/types/unotest-dsl.d.ts +218 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.46.0] - 2026-10-02
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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`.
|
|
8
|
+
- 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`.
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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.
|
|
13
|
+
- 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.
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
16
|
+
**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.
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
20
|
+
`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`.
|
|
21
|
+
|
|
22
|
+
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'] }`.
|
|
23
|
+
|
|
24
|
+
- 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).
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 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.
|
|
29
|
+
- Updated dependencies [8241cfb]
|
|
30
|
+
- Updated dependencies [0fa0343]
|
|
31
|
+
- @unotest/dsl@0.46.0
|
|
32
|
+
- @unotest/protocol@0.46.0
|
|
33
|
+
- @unotest/viewer@0.46.0
|
|
34
|
+
- @unotest/core@0.46.0
|
|
35
|
+
- @unotest/grounder-client@0.46.0
|
|
36
|
+
|
|
37
|
+
## [0.45.0] - 2026-10-01
|
|
38
|
+
|
|
39
|
+
### Minor Changes
|
|
40
|
+
|
|
41
|
+
- f8ed64e: Browser extensions in scenarios: `waitForServiceWorker({timeoutMs?, url?})` returns the URL of an MV3 extension's service worker, `evaluateInServiceWorker(js, ...args)` runs JS in it — the way to `chrome.storage`, `chrome.tabs` and `chrome.runtime`, with the same argument and return rules as `evaluate` — and `extensionId()` gives the id for `chrome-extension://<id>/popup.html`. Every call finds the worker anew, so a worker MV3 put to sleep does not go stale. Extensions load only into a browser started with `launch.userDataDir`; without it the calls fail at once and say so. `mockRoute` also answers the worker's own requests on a freshly opened profile.
|
|
42
|
+
- fabfa22: Two reads of one element: `boundingBox(loc)` returns `{x, y, width, height}` in viewport CSS pixels, or `null` when the element is not rendered; `evaluateOnLocator(loc, js, ...args)` runs JS on the matched element — the body gets the element first, then the extra arguments by the same rules as `evaluate`.
|
|
43
|
+
- 1f5441c: Network mocks in scenarios: `mockRoute(glob, response)` gives requests matching a Playwright URL glob a fixed answer for the rest of the scenario — a response (`{status?, body?, headers?, contentType?}`, an object body goes as JSON), `{abort: true}` or `{continue: true}`. Where several masks match, the one mocked last answers, so `mockRoute('**/*', {abort: true})` followed by `mockRoute('**/localhost:*/**', {continue: true})` is a network allowlist. `unmockRoute(glob?)` takes mocks off. `waitForRequest(glob, {timeoutMs?})` returns the request with `url`, `method`, `headers`, `postData` and `postDataJson`; on a mocked mask it also finds a request sent before the call. `{abort: true, record: true}` keeps what it stopped, and `assertNoRequest(glob, {since?})` fails naming those requests. A fixed response to `**/*` is refused, and the site's own page is never aborted. Captured network entries of mocked requests carry `mocked: true`.
|
|
44
|
+
- eead45c: New DSL functions `newPage(url?)` and `closePage(index?)`: a scenario opens and closes its own tabs. `newPage` opens a tab in the active browser context, makes it active and returns its index; with a url the tab opens it, a relative path resolving against the base URL as in `goto`. `waitForPage` never returns a tab `newPage` opened. `closePage` closes the active tab, or tab `index`. Closing the active tab makes the tab that was active before it active again, or the tab at the index below when that one is gone. Closing another tab keeps the steps where they are. The only open tab cannot be closed: the step fails with `cannot close the only open tab`.
|
|
45
|
+
- 0cb5c62: Four new DSL functions for reading and comparing values inside a scenario:
|
|
46
|
+
|
|
47
|
+
- `textMatches(text, /re/)` returns `false` on a miss, or `[whole, group1, …]` for the first match. A group that took no part is `null`. A string pattern is refused with a pointer to `textContains`.
|
|
48
|
+
- `parseJson(text)` parses a JSON string into a value you walk with `.` and `[]`. Invalid JSON, including an empty string, fails the step and quotes the input.
|
|
49
|
+
- `urlParam(url, key)` returns a decoded query parameter: `null` when the key is absent, `''` when it is empty. A path without a host works too.
|
|
50
|
+
- `assertEqual(actual, expected)` compares two values deeply, the way JSON does: key order does not count, and a missing key differs from a `null` one. A failure names the first differing path with both values (`values differ at panel.devices[1]: expected "ipad", got "iphone"`), followed by a line diff of the two sides.
|
|
51
|
+
|
|
52
|
+
- 1b833cf: New DSL functions `readFile(path, {encoding})` and `readFileInfo(path)`. `readFile` returns a file's text, or its bytes as base64 with `{encoding: 'base64'}`. `readFileInfo` returns `{size}` and, for a PNG, `png: {width, height}` read from the header, so a test can check an exported image without reading it whole. Both take a path relative to `sandbox.shellCwd` (the project root by default), an absolute path inside it, or the `path` that `waitForDownload()` returns, so a test can check the file a page downloaded. Any other path is refused, and so is a symlink inside the project that points out of it. `readJson` and `upload` now refuse such a symlink too; before, they followed it. The docs gain a "Files" section in "From @playwright/test".
|
|
53
|
+
- f2cabfa: New DSL function `readJson(path)`: reads a whole JSON file once and returns the parsed value, so a scenario can walk it with `.` and `[]`. It is meant for constants your app keeps in source: a small script exports them to JSON, and the scenario reads that file instead of repeating the values by hand. The path is relative to `sandbox.shellCwd` (the project root by default), the same base `readJsonLine` uses, and must stay inside it: an absolute path or one that climbs out with `../` is refused. A missing file or a file that is not valid JSON fails the step at once. The docs gain a "From @playwright/test" section on moving helpers and constants.
|
|
54
|
+
- 554891a: `setPage(index, {activate: false})` switches the tab the steps act on without bringing it to front, so a scenario can drive an extension panel in the background while the site stays the visible tab. Without the option `setPage` behaves as before and brings the tab to front. New DSL function `bringToFront(index?)` shows a tab explicitly and leaves the steps on the active tab; with no index it shows the active tab. An unknown option or a non-boolean `activate` fails the step instead of being ignored.
|
|
55
|
+
- d25af6b: `viewport: null` in the config turns size emulation off: the page is as wide as the real browser window (headless: the size from `--window-size` in `launch.args`). A `device` next to it is refused, and so is `setViewport(null)` in a scenario — Playwright keeps a size it set on an open page, so the real window is chosen in the config.
|
|
56
|
+
- 3bc4c8f: New DSL function `waitForDownload(options?)`: returns the first file the page downloaded that the scenario has not taken yet, as `{path, name}`. `path` is the absolute path of the saved copy; read it with `waitForFile`. `name` is the file name the site suggested. Downloads are recorded from the moment the browser opens, so clicking first and asking afterwards loses nothing. `{timeoutMs}` defaults to the navigation timeout. `{name}` takes a substring or regex of the file name; a download that doesn't match stays for a later call. In a run the file is saved with the run and the viewer lists it under the step that took it, as a link to the file. Outside a run it is saved under `~/.cache/unotest/web/downloads/`. The run journal has a new `download` event kind.
|
|
57
|
+
- 155ad28: New DSL function `waitForPage(options?)`: waits for a tab the scenario has not received yet, makes it active and returns its index. Use it for a tab whose index you don't know up front: one a browser extension opens, or one that opens next to tabs already there. A tab that opened before the call still counts, even when the click was a step earlier. No tab is returned twice: a tab counts as received when it was open at the start, when `setPage` switched to it, or when an earlier `waitForPage` returned it. `{timeoutMs}` defaults to the navigation timeout. When no new tab arrives in time, the step fails with `no new tab within N ms, N tabs open`. This replaces the `pause(ms)` workaround the docs used to recommend.
|
|
58
|
+
- a5a3825: `waitUntil(() => condition, {timeoutMs?, intervalMs?, label?})` polls a condition until it is truthy and returns that value — Playwright's `expect.poll`. The body is an expression or a block that returns its value; calls inside it (`apiCall`, `evaluate`, `textContent`) run on every try. `timeoutMs` defaults to the run's wait timeout, `intervalMs` to 250. Only a falsy value is retried: an error in the body fails the step at once. On timeout the step fails naming the label, the number of tries and the last value.
|
|
59
|
+
|
|
60
|
+
### Patch Changes
|
|
61
|
+
|
|
62
|
+
- a987021: Object literals in scenarios take a reserved word as a key, as in JavaScript: `mockRoute(glob, {continue: true})` used to fail to parse with `Expected property key (word or string)`, and so did any key such as `if`, `for`, `return` or `step`. Quoting the key (`{"continue": true}`) is no longer needed. Dot access by a reserved word (`a.continue`) still does not parse; use `a["continue"]`.
|
|
63
|
+
- f79f29b: `timeoutMs` is now accepted wherever `timeout` is: in actions (`click`, `fill`, `press`, `hover`, `selectOption`, `dragAndDrop` and the rest), navigation (`goto`, `reload`, `goBack`, `goForward`, `waitForUrl`, `waitForNavigation`), waits (`waitFor`, `waitForText`, `waitForCount`) and assertions (`assertText`, `assertVisible`, `assertHidden`, `assertValue`, `assertCount`, `assertUrl`). Before, `{timeoutMs: 5000}` there — the name the newer functions use — was ignored without a word and the call waited its default. Giving both `timeout` and `timeoutMs` in one call is refused.
|
|
64
|
+
- Updated dependencies [a987021]
|
|
65
|
+
- Updated dependencies [3bc4c8f]
|
|
66
|
+
- Updated dependencies [a5a3825]
|
|
67
|
+
- @unotest/dsl@0.45.0
|
|
68
|
+
- @unotest/protocol@0.45.0
|
|
69
|
+
- @unotest/viewer@0.45.0
|
|
70
|
+
- @unotest/core@0.45.0
|
|
71
|
+
- @unotest/grounder-client@0.45.0
|
|
72
|
+
|
|
3
73
|
## [0.44.0] - 2026-09-30
|
|
4
74
|
|
|
5
75
|
### Minor Changes
|
|
@@ -1454,7 +1524,7 @@ picker on` fetches this project's own access and writes the three grounder
|
|
|
1454
1524
|
|
|
1455
1525
|
That screen named one command:
|
|
1456
1526
|
|
|
1457
|
-
|
|
1527
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
1458
1528
|
|
|
1459
1529
|
In a project that has just been created, the command refuses with `no box
|
|
1460
1530
|
address` before it does anything: the project holds neither the box's address
|
|
@@ -1463,8 +1533,8 @@ address` before it does anything: the project holds neither the box's address
|
|
|
1463
1533
|
|
|
1464
1534
|
The screen now names both steps, in the order they have to happen:
|
|
1465
1535
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1536
|
+
npx @unotest/web box access larch
|
|
1537
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
1468
1538
|
|
|
1469
1539
|
The box's own name is filled in when its licence carries one. `box access`
|
|
1470
1540
|
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
|
|