@unotest/web 0.43.2 → 0.45.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 +7 -6
- package/.claude/skills/write-e2e-test-ground/SKILL.md +7 -6
- package/CHANGELOG.md +127 -3
- package/dist/config/schema.d.ts +260 -62
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +12 -3
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +241 -6
- 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-ClW2IRgY.d.ts → interfaces-OvnJKELk.d.ts} +232 -9
- 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/agent-integration.md +8 -0
- package/guides/dsl-reference.md +846 -10
- package/package.json +9 -8
- package/types/unotest-dsl.d.ts +199 -3
|
@@ -427,9 +427,10 @@ Assertions:
|
|
|
427
427
|
|
|
428
428
|
State reads:
|
|
429
429
|
- `getTitle()`, `getUrl()`, `getAttribute(loc, name)`, `getInnerText(loc)`,
|
|
430
|
-
`getInputValue(loc)`, `count(loc)
|
|
431
|
-
|
|
432
|
-
(`assertTrue(
|
|
430
|
+
`getInputValue(loc)`, `count(loc)`. A string you already hold answers
|
|
431
|
+
`s.length`, `s.trim()`, `s.includes(x)`, `s.startsWith(x)`
|
|
432
|
+
(`assertTrue(out.stdout.includes('ready'))`); `textContains(haystack,
|
|
433
|
+
needle)` is the same probe as a function.
|
|
433
434
|
- A `textarea` / `input` VALUE is not page text: `waitForText` and
|
|
434
435
|
`assertText` never see it. Check it with `assertValue(loc, expected)`
|
|
435
436
|
— it polls until the timeout (5 s by default, `{timeout}` to raise
|
|
@@ -440,9 +441,9 @@ State reads:
|
|
|
440
441
|
- Only the vocabulary above is expression language. `!` is rejected by
|
|
441
442
|
the parser (`unotest-web lint`: `parse error — Invalid token`);
|
|
442
443
|
`JSON.stringify` / `Date.now()` / `indexOf` and other JS globals and
|
|
443
|
-
string methods are rejected by the validator
|
|
444
|
-
named (`use json(value)`, `use nowMs()`, `use
|
|
445
|
-
needle)`). Express the check as an assertion or a locator
|
|
444
|
+
string methods beyond the four above are rejected by the validator
|
|
445
|
+
with the replacement named (`use json(value)`, `use nowMs()`, `use
|
|
446
|
+
s.includes(needle)`). Express the check as an assertion or a locator
|
|
446
447
|
(`assertHidden`, `assertCount`, `.filter({hasText})`, `textContains`).
|
|
447
448
|
|
|
448
449
|
Sandbox primitives:
|
|
@@ -558,9 +558,10 @@ Assertions:
|
|
|
558
558
|
|
|
559
559
|
State reads:
|
|
560
560
|
- `getTitle()`, `getUrl()`, `getAttribute(loc, name)`, `getInnerText(loc)`,
|
|
561
|
-
`getInputValue(loc)`, `count(loc)
|
|
562
|
-
|
|
563
|
-
(`assertTrue(
|
|
561
|
+
`getInputValue(loc)`, `count(loc)`. A string you already hold answers
|
|
562
|
+
`s.length`, `s.trim()`, `s.includes(x)`, `s.startsWith(x)`
|
|
563
|
+
(`assertTrue(out.stdout.includes('ready'))`); `textContains(haystack,
|
|
564
|
+
needle)` is the same probe as a function.
|
|
564
565
|
- A `textarea` / `input` VALUE is not page text: `waitForText` and
|
|
565
566
|
`assertText` never see it. Check it with `assertValue(loc, expected)`
|
|
566
567
|
— it polls until the timeout (5 s by default, `{timeout}` to raise
|
|
@@ -571,9 +572,9 @@ State reads:
|
|
|
571
572
|
- Only the vocabulary above is expression language. `!` is rejected by
|
|
572
573
|
the parser (`unotest-web lint`: `parse error — Invalid token`);
|
|
573
574
|
`JSON.stringify` / `Date.now()` / `indexOf` and other JS globals and
|
|
574
|
-
string methods are rejected by the validator
|
|
575
|
-
named (`use json(value)`, `use nowMs()`, `use
|
|
576
|
-
needle)`). Express the check as an assertion or a locator
|
|
575
|
+
string methods beyond the four above are rejected by the validator
|
|
576
|
+
with the replacement named (`use json(value)`, `use nowMs()`, `use
|
|
577
|
+
s.includes(needle)`). Express the check as an assertion or a locator
|
|
577
578
|
(`assertHidden`, `assertCount`, `.filter({hasText})`, `textContains`).
|
|
578
579
|
|
|
579
580
|
Sandbox primitives:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,129 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.45.0] - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- 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`.
|
|
9
|
+
- 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`.
|
|
10
|
+
- 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`.
|
|
11
|
+
- 0cb5c62: Four new DSL functions for reading and comparing values inside a scenario:
|
|
12
|
+
|
|
13
|
+
- `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`.
|
|
14
|
+
- `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.
|
|
15
|
+
- `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.
|
|
16
|
+
- `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.
|
|
17
|
+
|
|
18
|
+
- 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".
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 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"]`.
|
|
29
|
+
- 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.
|
|
30
|
+
- Updated dependencies [a987021]
|
|
31
|
+
- Updated dependencies [3bc4c8f]
|
|
32
|
+
- Updated dependencies [a5a3825]
|
|
33
|
+
- @unotest/dsl@0.45.0
|
|
34
|
+
- @unotest/protocol@0.45.0
|
|
35
|
+
- @unotest/viewer@0.45.0
|
|
36
|
+
- @unotest/core@0.45.0
|
|
37
|
+
- @unotest/grounder-client@0.45.0
|
|
38
|
+
|
|
39
|
+
## [0.44.0] - 2026-09-30
|
|
40
|
+
|
|
41
|
+
### Minor Changes
|
|
42
|
+
|
|
43
|
+
- eb7be5d: Page clock, time zone and locale.
|
|
44
|
+
|
|
45
|
+
- **New DSL functions `clockFreeze(iso)`, `clockTick(ms)`, `clockRun()`.** `clockFreeze` stops time on the page: `Date` reads the given instant, and `setTimeout`, `setInterval` and `requestAnimationFrame` stand still. A "typing…" indicator or a countdown then gives the same screenshot on every run. `clockTick` moves frozen time forward and fires the timers that fall due. `clockRun` lets time run again. `clockFreeze` works at any point, including after `goto`, and takes past dates as well as future ones.
|
|
46
|
+
- **When the clock is installed.** A run puts the fake clock on the browser before the first page loads, and only for scenarios that can reach a clock call, directly or through a helper. Every other scenario keeps real time, as before. Playwright's spelling (`clock.freeze(…)`, `pauseAt`, `runFor`, `resume`) is refused with the DSL name to use instead.
|
|
47
|
+
- **MCP.** `new_context` and `explore_start` take `clock: true`. `explore_step` gains `clock_freeze {time}`, `clock_tick {ms}` and `clock_run`, recorded as the DSL calls. On a browser opened without the clock they are refused, and the refusal says how to open one with it.
|
|
48
|
+
- **Config `timezone` and `locale`** (`"Europe/Madrid"`, `"es-ES"`) for every page. `UNOTEST_TIMEZONE` and `UNOTEST_LOCALE` still override them for one run. An unknown zone or a malformed locale is refused when the config loads.
|
|
49
|
+
- Screenshots taken while time is frozen no longer wait for an animation frame that cannot come. Each used to cost up to two seconds.
|
|
50
|
+
|
|
51
|
+
- 816e2ea: A second browser context in one test: two clients of the same app.
|
|
52
|
+
|
|
53
|
+
- **New DSL functions `newContext()` and `setContext(index)`.** `newContext()` opens another browser context with the same options as the first: base URL, viewport, `device`, time zone, locale and page clock. It has its own cookies, storage and tabs, makes it the active one and returns its number (the test's own context is `0`). `setContext(i)` switches. Each context keeps its own tab and frame, so a switch back lands where the scenario left off. At the end of the test every context is closed.
|
|
54
|
+
- **Config `maxContexts`** (default 4) caps contexts per test. The next `newContext()` is refused and the refusal names the key. On a browser profile (`launch.userDataDir`) `newContext()` is refused: every page there shares one context.
|
|
55
|
+
- **Run journal and failure bundle.** Once a second context exists, each step records the context it started in (`contextIndex`), and the viewer's Trace shows it as a `ctx N` chip. A failure bundles the active context's page, console and network, with `contextIndex` in `failure.json`. A one-context test writes exactly what it did before. Both fields are additive: an older viewer reads the new journal and a newer one reads the old.
|
|
56
|
+
- **MCP.** `explore_step` gains `newContext` and `setContext {index}`, recorded as the same DSL calls. The `new_context` tool still resets the browser. Intent grounding follows the active context. An agent joining a paused debug run (`take_wheel`, `attach_debug_session`) lands in the context the run is on (Chromium).
|
|
57
|
+
- **New lint rule `lint:shadows-dsl-function`** (warning): a function or variable of your own that has a DSL function's name.
|
|
58
|
+
|
|
59
|
+
**If your scenarios define their own `newContext` or `setContext`:** a call by that name still runs your function, so nothing changes at run time. The new DSL function is out of reach until you rename yours. `lint:shadows-dsl-function` points at the definition.
|
|
60
|
+
|
|
61
|
+
- 565ef67: Strings, object literals and indexes in scenarios.
|
|
62
|
+
|
|
63
|
+
- A string value answers `s.length`, `s.trim()`, `s.includes(search)` and `s.startsWith(prefix)` — in asserts, in conditions, on literals and on call results (`assertTrue(getTitle().trim().length > 0)`). Any other member (`s.padStart(3)`) is a lint error, `validator:unsupported-string-member`, that names it and lists the four. A member of the list used on a value that is not a string at run time fails the step with the value's type in the message.
|
|
64
|
+
- An object literal is a value: `h = {x: 1, y: "z"}` can be kept in a variable, nested (`{a: {b: [1]}}`) and passed to a helper. A slot that wants a specific kind (a string, a locator) still rejects an object with its `arg-kind` error.
|
|
65
|
+
- An index works anywhere in a chain: `res.body.items[0].id`, `dbQuery(sql)[0].id`, `a.b[1][0]` — no temporary variable. Indexing a locator is still rejected.
|
|
66
|
+
- The lint hints for JS string habits now point at the string members (`indexOf` → `use s.includes(needle)`).
|
|
67
|
+
|
|
68
|
+
- 5ac6c72: `teardown("label", () => { … })` — give the stand back whatever happens.
|
|
69
|
+
|
|
70
|
+
- A scenario registers a rollback block where it turns something on; after the test body every registered block runs, newest first, whether the test passed, failed, ran out of time or was stopped — including a run stopped while paused at its failure (the debugger, `run_test`). One failing block does not stop the others. Flows and helpers can carry their own rollback.
|
|
71
|
+
- A green test whose teardown failed is failed; a failed test keeps its own error and lists the teardown failures next to it (CLI, `run_test` → `next.teardownFailures`, `runtime.json`). The failure bundle is taken before the rollback, so it shows the page the test broke on.
|
|
72
|
+
- The CLI prints `↺ teardown "…"` per block; the journal gets `teardown:started` / `teardown:finished` / `teardown:skipped` events and a `teardown` flag on the block frame; the viewer marks those frames and shows "returning the stand…" while a stopped run rolls back.
|
|
73
|
+
- Stop now rolls back first: the first Stop (viewer, `abort_runtime`, SIGTERM, Ctrl-C) plays the teardown blocks and then ends the run; a second Stop skips what is left and says so. `abort_runtime` answers `status: "returning the stand…"` while that happens.
|
|
74
|
+
- New config key `teardownTimeoutMs` (default 60000): the ceiling for all teardown blocks of a run, separate from the run's own time budget. A killed process, or blocks cut by this ceiling, leave the stand as the test set it — the run result names them.
|
|
75
|
+
- Lint: `teardown-shape` (string-literal label, no `return` / `break` out of the block, no nested teardown) and `teardown-name-conflict`.
|
|
76
|
+
|
|
77
|
+
**Breaking for scenarios that define their own `teardown`.** A function, parameter or variable named `teardown` is now a lint error (`teardown-name-conflict`), and a statement-level call `teardown(x)` no longer parses as a call. Rename it, e.g. to `undo_setup`.
|
|
78
|
+
|
|
79
|
+
- d1605b2: `assertScreenshot` — the page, or an element, against a PNG baseline in the repo.
|
|
80
|
+
|
|
81
|
+
- `assertScreenshot(name, options?)` / `assertScreenshot(locator, name, options?)` compares a snapshot with `unotest/e2e/<scenario>.screenshots/<name>.png` — a Figma export or a snapshot you accepted. A pixel differs when any channel differs by more than `threshold` (default 32); the assert fails when the share of differing pixels is more than `maxDiffRatio` (default 0) — exactly the limit passes. `mask: [locator…]` and `ignore: [{x, y, width, height}…]` leave areas out of the comparison on both sides. A different size fails at once with both sizes; nothing is resampled. `fullPage: true` compares the whole page.
|
|
82
|
+
- No baseline yet: the assert fails and the snapshot is saved in the run's artifacts as a candidate under the baseline's file name. `e2e --update-screenshots` (MCP: `run_test {updateScreenshots: true}`) writes the snapshots as baselines; it is refused on a box.
|
|
83
|
+
- A failed assert keeps `expected`, `actual` and `diff` pictures (per retry attempt); the viewer shows them side by side on the failed step with a "Download as baseline" button, the failure bundle points at them, and `run_test` returns them in `next.visual`. `box run --download` / `box_run_download` unpack baseline candidates next to the zip.
|
|
84
|
+
- A baseline is read as untrusted input: an image over 25 megapixels, or one whose data unpacks to more than its header allows, is refused before it is decoded.
|
|
85
|
+
- The journal gets a `visual-assert` event per assert; `runtime.json` carries the last failed one as `visual`.
|
|
86
|
+
- A suite over 128 MB is warned about before the upload — boxes accept 128 MB by default — naming its biggest files and baselines.
|
|
87
|
+
|
|
88
|
+
New dependency of `@unotest/web`: `pngjs` 7.0.0 (MIT, no dependencies of its own), the PNG codec behind the comparison.
|
|
89
|
+
|
|
90
|
+
- 3939ae6: Window size and scale: set them in the config, per MCP call, or mid-scenario.
|
|
91
|
+
|
|
92
|
+
- **`viewport.deviceScaleFactor`** in `unotest.config`. `{ width: 390, height: 844, deviceScaleFactor: 2 }` makes every screenshot 780×1688, the scale design mockups are usually drawn at. Every `viewport` field is now optional: a missing one comes from `device` or from the 1280×720 default. A config without `viewport` opens at 1280×720, as before.
|
|
93
|
+
- **`device`** in the config: a Playwright device preset by its exact name (`"iPhone 14"`, `"Pixel 7"`), applied whole — size, scale, touch, mobile mode, user agent. `viewport` next to it overrides the preset field by field. An unknown name is refused with the nearest ones. A mobile preset on Firefox is refused: Firefox has no mobile mode. A preset's viewport is the page area, smaller than the screen: `"iPhone 14"` is 390×664. To match a 390×844 mockup, set `viewport` explicitly.
|
|
94
|
+
- **MCP `new_context`** takes `viewport` and `device`, and opens the context at that window. **`explore_start`** takes them too when no browser is open yet. With a browser already open it refuses and points to `new_context`. Before this, `new_context` accepted any argument, ignored it and answered success. Now an unknown argument is refused and the reply names the accepted fields.
|
|
95
|
+
- **New DSL function `setViewport({width, height})`.** It resizes every open tab in place, with no reload, and tabs opened later start at the same size. `deviceScaleFactor` is refused here: the scale is fixed when the browser opens.
|
|
96
|
+
|
|
97
|
+
### Patch Changes
|
|
98
|
+
|
|
99
|
+
- 3780074: A finished run opened from history keeps its rollback pass: the viewer no longer drops `teardown:started` / `teardown:finished` / `teardown:skipped` from a stored journal, so the teardown frames of a stopped run show next to the step it was stopped in — not inside it — as they do while the run is live.
|
|
100
|
+
- 99ba57f: The run budget (`UNOTEST_MAX_DURATION_MS`, `maxDurationMs`) is now a deadline, not a check between statements.
|
|
101
|
+
|
|
102
|
+
- **Behaviour change:** a run whose last action took it past the budget used to finish green, and so did a run whose single action outlasted the whole budget. Both are now red with `maxDurationMs budget (…ms) exhausted`, reported on the action that spent it.
|
|
103
|
+
- An action still running when the budget runs out is abandoned at that moment instead of being waited for. Its browser call is not killed: it ends at its own timeout or when the context closes, and its late failure is not reported. Teardown blocks still run afterwards, on their own `teardownTimeoutMs`.
|
|
104
|
+
- Every budget now fails with a typed `BudgetExhaustedError` (`setting: "maxSteps" | "maxDurationMs" | "teardownTimeoutMs"`, `kind: "steps" | "duration"`). The message names the setting that was spent: a teardown cut by its own ceiling says `teardownTimeoutMs (…ms) exhausted — the rollback did not finish`, as before, not the run's budget.
|
|
105
|
+
|
|
106
|
+
- d66cf72: `readBounded(handle, size, max)` reads an open file into a buffer of its measured size plus one byte: a file over `max` is refused before anything is allocated, and one that grew after it was measured is refused rather than read in part (`BoundedReadError`). Reading a screenshot baseline no longer reserves memory for the largest file allowed — only for the file there is.
|
|
107
|
+
- 832c101: `resume` and `step` now answer with the stop that comes after the command, not the pause the run was already in.
|
|
108
|
+
|
|
109
|
+
A paused run keeps refreshing its runtime file, and the tools took a fresh timestamp on a paused state for the next pause. An agent could be told the run had moved on while it still sat at the old breakpoint, and take the wheel there. The runner now counts its pauses in `runtime.json` (`pauseSeq`, one more on every pause, the same line again included), and `resume` / `step` wait for that count to grow or for the run to end. A run from an older runner, without the count, is waited on as before.
|
|
110
|
+
|
|
111
|
+
- b613684: A screenshot the browser refuses to capture is asked for once more. Chromium now and then refuses a frame on a busy machine, and `assertScreenshot`, `screenshot()` and the MCP screenshot step used to fail on the first refusal with a raw protocol error. A second refusal is now `ScreenCaptureError`, which names the browser's reason and what to do. A closed page or a timeout is not retried.
|
|
112
|
+
- e417c08: A stopped run never ends silently: if the first Stop's rollback — or what runs before it, such as framing a paused failure — is still going at `teardownTimeoutMs` plus 15 seconds, the runner prints that it is exiting without finishing the rollback, then exits. A rollback block cut as hung is named in the output. The DSL reference now states both Stop limits: the rollback budget with its 15-second exit margin, and the viewer's 5-minute force-kill as a safety net only.
|
|
113
|
+
- Updated dependencies [816e2ea]
|
|
114
|
+
- Updated dependencies [565ef67]
|
|
115
|
+
- Updated dependencies [5ac6c72]
|
|
116
|
+
- Updated dependencies [d1605b2]
|
|
117
|
+
- Updated dependencies [3780074]
|
|
118
|
+
- Updated dependencies [99ba57f]
|
|
119
|
+
- Updated dependencies [d66cf72]
|
|
120
|
+
- Updated dependencies [832c101]
|
|
121
|
+
- @unotest/protocol@0.44.0
|
|
122
|
+
- @unotest/viewer@0.44.0
|
|
123
|
+
- @unotest/dsl@0.44.0
|
|
124
|
+
- @unotest/core@0.44.0
|
|
125
|
+
- @unotest/grounder-client@0.44.0
|
|
126
|
+
|
|
3
127
|
## [0.43.2] - 2026-09-30
|
|
4
128
|
|
|
5
129
|
### Patch Changes
|
|
@@ -1366,7 +1490,7 @@ picker on` fetches this project's own access and writes the three grounder
|
|
|
1366
1490
|
|
|
1367
1491
|
That screen named one command:
|
|
1368
1492
|
|
|
1369
|
-
|
|
1493
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
1370
1494
|
|
|
1371
1495
|
In a project that has just been created, the command refuses with `no box
|
|
1372
1496
|
address` before it does anything: the project holds neither the box's address
|
|
@@ -1375,8 +1499,8 @@ address` before it does anything: the project holds neither the box's address
|
|
|
1375
1499
|
|
|
1376
1500
|
The screen now names both steps, in the order they have to happen:
|
|
1377
1501
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1502
|
+
npx @unotest/web box access larch
|
|
1503
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
1380
1504
|
|
|
1381
1505
|
The box's own name is filled in when its licence carries one. `box access`
|
|
1382
1506
|
with no name refuses, and its refusal offers to create a box — to somebody who
|