@unotest/web 0.43.1 → 0.44.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.
@@ -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)`; `textContains(haystack, needle)`
431
- for a substring probe on a string you already hold
432
- (`assertTrue(textContains(out.stdout, 'ready'))`).
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 with the replacement
444
- named (`use json(value)`, `use nowMs()`, `use textContains(haystack,
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)`; `textContains(haystack, needle)`
562
- for a substring probe on a string you already hold
563
- (`assertTrue(textContains(out.stdout, 'ready'))`).
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 with the replacement
575
- named (`use json(value)`, `use nowMs()`, `use textContains(haystack,
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,167 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.44.0] - 2026-09-30
4
+
5
+ ### Minor Changes
6
+
7
+ - eb7be5d: Page clock, time zone and locale.
8
+
9
+ - **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.
10
+ - **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.
11
+ - **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.
12
+ - **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.
13
+ - Screenshots taken while time is frozen no longer wait for an animation frame that cannot come. Each used to cost up to two seconds.
14
+
15
+ - 816e2ea: A second browser context in one test: two clients of the same app.
16
+
17
+ - **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.
18
+ - **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.
19
+ - **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.
20
+ - **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).
21
+ - **New lint rule `lint:shadows-dsl-function`** (warning): a function or variable of your own that has a DSL function's name.
22
+
23
+ **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.
24
+
25
+ - 565ef67: Strings, object literals and indexes in scenarios.
26
+
27
+ - 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.
28
+ - 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.
29
+ - 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.
30
+ - The lint hints for JS string habits now point at the string members (`indexOf` → `use s.includes(needle)`).
31
+
32
+ - 5ac6c72: `teardown("label", () => { … })` — give the stand back whatever happens.
33
+
34
+ - 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.
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+ - 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.
39
+ - Lint: `teardown-shape` (string-literal label, no `return` / `break` out of the block, no nested teardown) and `teardown-name-conflict`.
40
+
41
+ **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`.
42
+
43
+ - d1605b2: `assertScreenshot` — the page, or an element, against a PNG baseline in the repo.
44
+
45
+ - `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.
46
+ - 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.
47
+ - 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.
48
+ - 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.
49
+ - The journal gets a `visual-assert` event per assert; `runtime.json` carries the last failed one as `visual`.
50
+ - A suite over 128 MB is warned about before the upload — boxes accept 128 MB by default — naming its biggest files and baselines.
51
+
52
+ New dependency of `@unotest/web`: `pngjs` 7.0.0 (MIT, no dependencies of its own), the PNG codec behind the comparison.
53
+
54
+ - 3939ae6: Window size and scale: set them in the config, per MCP call, or mid-scenario.
55
+
56
+ - **`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.
57
+ - **`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.
58
+ - **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.
59
+ - **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.
60
+
61
+ ### Patch Changes
62
+
63
+ - 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.
64
+ - 99ba57f: The run budget (`UNOTEST_MAX_DURATION_MS`, `maxDurationMs`) is now a deadline, not a check between statements.
65
+
66
+ - **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.
67
+ - 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`.
68
+ - 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.
69
+
70
+ - 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.
71
+ - 832c101: `resume` and `step` now answer with the stop that comes after the command, not the pause the run was already in.
72
+
73
+ 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.
74
+
75
+ - 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.
76
+ - 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.
77
+ - Updated dependencies [816e2ea]
78
+ - Updated dependencies [565ef67]
79
+ - Updated dependencies [5ac6c72]
80
+ - Updated dependencies [d1605b2]
81
+ - Updated dependencies [3780074]
82
+ - Updated dependencies [99ba57f]
83
+ - Updated dependencies [d66cf72]
84
+ - Updated dependencies [832c101]
85
+ - @unotest/protocol@0.44.0
86
+ - @unotest/viewer@0.44.0
87
+ - @unotest/dsl@0.44.0
88
+ - @unotest/core@0.44.0
89
+ - @unotest/grounder-client@0.44.0
90
+
91
+ ## [0.43.2] - 2026-09-30
92
+
93
+ ### Patch Changes
94
+
95
+ - 1547ae7: `abort_runtime` now takes a run that is still waiting in the queue out of it. It used to answer ok for such a run while its place in line stayed, and the run started later all the same. It now removes the run from the queue and answers `{ok: true, state: "withdrawn"}`; nothing is run. A run that was taken out of the queue, here or in the viewer's Active panel, no longer ends its `unotest-web e2e` process with a stack trace: it prints that it was taken out and that nothing was run, and exits with 130, as Ctrl-C does.
96
+
97
+ What changes for you: `abort_runtime` on a runtime id with no run behind
98
+ it at all now answers `no-runner`, marked as an error, as `take_wheel` and
99
+ `release_wheel` already do, instead of `ok`. A finished run still answers
100
+ `ok`. A queued run taken out of the queue exits with 130 instead of 1.
101
+
102
+ - c04cf8b: When a person answered a request is now shown outside the cabinet too. The
103
+ cloud API adds `decidedAt` to an approved, declined or failed request. The
104
+ `cloud_approval_wait` MCP tool returns it, and the CLI prints it — "approved
105
+ in the cabinet at 2026-09-27 09:14 UTC" — in `approvals <id>` and while
106
+ `box create`, `box destroy` and `box picker on` wait; with `--json` it is an
107
+ `approval` field in the object the command already prints. A request that
108
+ expired shows the deadline it was last given, marked as such, since the
109
+ cloud's answer for an expired request carries no time. A service that does
110
+ not send the field yet is read as before, without a time.
111
+ - 2e70ce0: The times the CLI and the MCP tools show for an approval request (when a person answered it, when it expires) are now only taken from the service when they are a real date and time. Anything else is treated as if the service had sent no time: nothing is printed for it, and an expired request names no deadline it cannot vouch for.
112
+ - 665da8f: `box runs` and the other box read commands now say when the token is of the wrong kind (a project push token instead of a read token) and how to get the right one, instead of reporting that the box did not list its environments.
113
+ - 3639067: Text the cloud or a box sends can no longer send instructions to your terminal. The `login`, `logout`, `whoami`, `approvals`, `box`, `picker`, `bundle` and `env` commands used to print box names, request ids, error messages and other text from the service as they came, so control characters in them, from a service with a bug or a `UNOTEST_CLOUD_URL` pointed elsewhere, could clear your screen or retitle the window. They are now shown escaped (`\x1b`), and line breaks stay line breaks.
114
+ - 0b29243: `explore_run_flow` now says where a failed replay broke. The reply used to carry the error message alone, so a flow that failed inside a helper gave no file and no line, and the agent had to work out the cause from a screenshot. A failed replay now answers JSON: `error` (the same text as before), `flow`, `at` with the file, line and column of the statement that failed (the helper's file when it failed inside one, project-relative), its step label and the call chain from the flow down, and `artifacts` with a capture of the page as the flow left it.
115
+
116
+ What changes for you: a failed `explore_run_flow` answers JSON instead of
117
+ plain text, with the old text under `error`. Helpers now load the way
118
+ `run_test` loads them, so a function defined in two helper files is refused,
119
+ naming both files, in `explore_run_flow` as it already was in `run_test`.
120
+
121
+ - 7a0671e: An agent without a grounder is now told how to get one. When intent locators have no grounder (ollama not running, grounding off), `explore_start`'s `grounder` and a failed intent step carry `{state: "no-grounder", next, orElse?, why}`: `next` is the Picker command (`box picker on` for a project with a box, `login && picker on` without one), `orElse` is `ollama serve` in local mode, and `why` says the MCP server needs a reconnect after the Picker is turned on. A remote grounder that is configured but down gets the reason only, since turning a Picker on would not fix it. With `UNOTEST_SNAPSHOT=0`, `explore_start` now says the page snapshot tools are not served, how to bring them back, and how to locate elements in the meantime. The server instructions describe the new state.
122
+
123
+ What changes for you: an intent step that fails for want of a grounder now
124
+ answers `{status: "failed", error, grounder}` like other steps that fail
125
+ before they act, instead of an error reply carrying the text alone; the
126
+ `error` text is unchanged.
127
+
128
+ - ebcfbe0: A `run_test` that waits in the queue now says who holds the machine. The `queued` reply used to say only that the machine was busy, so an agent could not tell whether to wait or to stop a run of its own it no longer needed. It now carries `heldBy`: each run holding the machine with its run id, kind, scenario or collection name, how long it has been running (left out while that is not known yet) and, for a scenario run, the `abort_runtime` call that stops it. A blocking `run_test` (the default) used to wait for its turn for up to three minutes, longer than an MCP client waits for any answer, so the agent was told only that the request timed out. It now waits up to 30 seconds for a place and then answers `queued` with `heldBy`, the same as `wait: false`; the run stays in the queue and starts on its own. `abort_runtime` takes a queued run back out.
129
+
130
+ What changes for you: a blocking `run_test` behind a busy machine may now
131
+ answer `status: "queued"` after about 30 seconds instead of waiting for the
132
+ run to start; call `inspect_runtime` on its `runtimeId` to follow it.
133
+
134
+ - 6e4d598: A tool that throws something other than an `Error`, such as a plain string,
135
+ now has registered secrets masked in the message the agent reads, as an
136
+ `Error` already did.
137
+ - 56c899c: `resume` and `step` now answer with the stop that comes after the command, not the pause the run was already in.
138
+
139
+ 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.
140
+
141
+ This ships as a patch: it fixes how the existing `resume` and `step` wait, and adds no new tool, flag or input. `pauseSeq` is an optional field that only ever appears in addition: an older reader ignores it, and a newer reader facing a file without it keeps the previous timestamp check.
142
+
143
+ - 70c37be: Screenshots and saved page HTML leave out what a page keeps secret.
144
+
145
+ - Password and one-time-code fields are painted over on every screenshot: the agent's screenshots, per-step captures, and the screenshot taken when a run fails.
146
+ - The `page.html` saved when a run fails no longer contains the content of `<script>` elements or the values of hidden form fields; each is replaced by a label with its length. The same applies to the element HTML written in debug mode.
147
+ - Text in elements that are not rendered, and in `noembed` and `noframes`, no longer reaches element names.
148
+
149
+ What changes for you: if you read `page.html` for script data or hidden field values, set `failureBundle.pageHtml: "full"` in your config to keep them. Password fields stay hidden either way.
150
+
151
+ - 0e15c53: An element's text, wherever the page is read for it — the locator a ref becomes in a recorded scenario, `check_locator`'s matches and the failure's semantic snapshot — no longer includes the content of `<noscript>`, `<script>`, `<style>` or `<template>`: with JavaScript on, a page could repeat a secret inside `<noscript>` and it went into the customer's scenario (or into the refusal the agent read) in a locator that, since Playwright's own text matching skips `<noscript>`, did not even find the element.
152
+
153
+ The same goes for a failed `assertText` (and `assert_text`): it still compares against the element's full text, as before, but the "got" it quotes — in the MCP reply, the run's output, `runtime.json`, `steps.jsonl` and `failure.json` — leaves those blocks out, and when they alone made the comparison fail it says so. **What changes for you:** a failure message may now read `got "API key"` where it used to show the text of a `<noscript>` as well, followed by a note that the comparison includes hidden text; very long texts are quoted up to 10,000 characters, with a note when cut.
154
+
155
+ - Updated dependencies [665da8f]
156
+ - Updated dependencies [56c899c]
157
+ - Updated dependencies [70c37be]
158
+ - Updated dependencies [0e15c53]
159
+ - @unotest/core@0.43.2
160
+ - @unotest/protocol@0.43.2
161
+ - @unotest/grounder-client@0.43.2
162
+ - @unotest/dsl@0.43.2
163
+ - @unotest/viewer@0.43.2
164
+
3
165
  ## [0.43.1] - 2026-09-29
4
166
 
5
167
  ### Patch Changes
@@ -1292,7 +1454,7 @@ picker on` fetches this project's own access and writes the three grounder
1292
1454
 
1293
1455
  That screen named one command:
1294
1456
 
1295
- npx @unotest/web bundle push --run --env ENVIRONMENT
1457
+ npx @unotest/web bundle push --run --env ENVIRONMENT
1296
1458
 
1297
1459
  In a project that has just been created, the command refuses with `no box
1298
1460
  address` before it does anything: the project holds neither the box's address
@@ -1301,8 +1463,8 @@ address` before it does anything: the project holds neither the box's address
1301
1463
 
1302
1464
  The screen now names both steps, in the order they have to happen:
1303
1465
 
1304
- npx @unotest/web box access larch
1305
- npx @unotest/web bundle push --run --env ENVIRONMENT
1466
+ npx @unotest/web box access larch
1467
+ npx @unotest/web bundle push --run --env ENVIRONMENT
1306
1468
 
1307
1469
  The box's own name is filled in when its licence carries one. `box access`
1308
1470
  with no name refuses, and its refusal offers to create a box — to somebody who
@@ -20,14 +20,19 @@ declare const BROWSER_CHANNEL_WORDS: readonly ["chrome", "msedge", "chrome-beta"
20
20
  declare const BrowserChannelSchema: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["chrome", "msedge", "chrome-beta", "chromium"]>, z.ZodEffects<z.ZodLiteral<"bundled">, null, "bundled">, z.ZodNull]>>;
21
21
  type BrowserChannel = z.infer<typeof BrowserChannelSchema>;
22
22
  declare const ViewportSchema: z.ZodObject<{
23
- width: z.ZodNumber;
24
- height: z.ZodNumber;
23
+ width: z.ZodOptional<z.ZodNumber>;
24
+ height: z.ZodOptional<z.ZodNumber>;
25
+ /** CSS pixel → device pixel ratio: 2 makes a 390×844 page a 780×1688
26
+ * screenshot, the scale design mockups are usually drawn at. */
27
+ deviceScaleFactor: z.ZodOptional<z.ZodNumber>;
25
28
  }, "strict", z.ZodTypeAny, {
26
- width: number;
27
- height: number;
29
+ width?: number | undefined;
30
+ height?: number | undefined;
31
+ deviceScaleFactor?: number | undefined;
28
32
  }, {
29
- width: number;
30
- height: number;
33
+ width?: number | undefined;
34
+ height?: number | undefined;
35
+ deviceScaleFactor?: number | undefined;
31
36
  }>;
32
37
  type Viewport = z.infer<typeof ViewportSchema>;
33
38
  declare const RetryReasonSchema: z.ZodEnum<["transient", "network", "crash"]>;
@@ -69,6 +74,11 @@ declare const FailureBundleConfigSchema: z.ZodObject<{
69
74
  days: number;
70
75
  }>;
71
76
  storageDir: z.ZodString;
77
+ /** What page.html keeps. `redacted`: a script's content and a hidden
78
+ * field's value become a label with their length. `full`: both as the
79
+ * page had them — an app's bootstrap data and request-forgery tokens
80
+ * included. A secret field's value is hidden either way. */
81
+ pageHtml: z.ZodEnum<["redacted", "full"]>;
72
82
  }, "strict", z.ZodTypeAny, {
73
83
  tier1: true;
74
84
  tier2: boolean;
@@ -81,6 +91,7 @@ declare const FailureBundleConfigSchema: z.ZodObject<{
81
91
  days: number;
82
92
  };
83
93
  storageDir: string;
94
+ pageHtml: "redacted" | "full";
84
95
  }, {
85
96
  tier2: boolean;
86
97
  tier3: {
@@ -92,24 +103,25 @@ declare const FailureBundleConfigSchema: z.ZodObject<{
92
103
  days: number;
93
104
  };
94
105
  storageDir: string;
106
+ pageHtml: "redacted" | "full";
95
107
  tier1?: true | undefined;
96
108
  }>;
97
109
  type FailureBundleConfig = z.infer<typeof FailureBundleConfigSchema>;
98
110
  declare const DialogPolicySchema: z.ZodEnum<["accept", "dismiss", "manual"]>;
99
111
  type DialogPolicy = z.infer<typeof DialogPolicySchema>;
100
- declare const LinterRuleIdSchema: z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "lint:goto-concat", "lint:regex-invalid", "lint:echo-assert", "lint:api-call-file-body", "lint:lint-ok-missing-reason", "lint:flow-returns-value", "lint:legacy-layout", "lint:external-variable-undeclared", "lint:env-host-from-base", "lint:one-test-per-file", "validator:unknown-function", "validator:arity", "validator:arg-kind", "validator:step-coverage", "validator:step-shape", "validator:meta-shape", "validator:function-shape", "validator:for-shape", "validator:if-shape", "validator:var-shape", "validator:unsupported-statement", "validator:unsupported-expression", "validator:string-concat", "validator:semantic-loss", "validator:list-arg", "validator:wrapped-format"]>;
112
+ declare const LinterRuleIdSchema: z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "lint:goto-concat", "lint:regex-invalid", "lint:echo-assert", "lint:api-call-file-body", "lint:lint-ok-missing-reason", "lint:flow-returns-value", "lint:legacy-layout", "lint:external-variable-undeclared", "lint:env-host-from-base", "lint:one-test-per-file", "lint:shadows-dsl-function", "validator:unknown-function", "validator:arity", "validator:arg-kind", "validator:step-coverage", "validator:step-shape", "validator:meta-shape", "validator:function-shape", "validator:for-shape", "validator:if-shape", "validator:var-shape", "validator:unsupported-statement", "validator:unsupported-expression", "validator:string-concat", "validator:semantic-loss", "validator:list-arg", "validator:wrapped-format"]>;
101
113
  type LinterRuleId = z.infer<typeof LinterRuleIdSchema>;
102
114
  declare const LinterSeveritySchema: z.ZodEnum<["off", "warn", "error"]>;
103
115
  type LinterSeverity = z.infer<typeof LinterSeveritySchema>;
104
116
  declare const LinterConfigSchema: z.ZodObject<{
105
117
  enabled: z.ZodBoolean;
106
- rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "lint:goto-concat", "lint:regex-invalid", "lint:echo-assert", "lint:api-call-file-body", "lint:lint-ok-missing-reason", "lint:flow-returns-value", "lint:legacy-layout", "lint:external-variable-undeclared", "lint:env-host-from-base", "lint:one-test-per-file", "validator:unknown-function", "validator:arity", "validator:arg-kind", "validator:step-coverage", "validator:step-shape", "validator:meta-shape", "validator:function-shape", "validator:for-shape", "validator:if-shape", "validator:var-shape", "validator:unsupported-statement", "validator:unsupported-expression", "validator:string-concat", "validator:semantic-loss", "validator:list-arg", "validator:wrapped-format"]>, z.ZodEnum<["off", "warn", "error"]>>;
118
+ rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "lint:goto-concat", "lint:regex-invalid", "lint:echo-assert", "lint:api-call-file-body", "lint:lint-ok-missing-reason", "lint:flow-returns-value", "lint:legacy-layout", "lint:external-variable-undeclared", "lint:env-host-from-base", "lint:one-test-per-file", "lint:shadows-dsl-function", "validator:unknown-function", "validator:arity", "validator:arg-kind", "validator:step-coverage", "validator:step-shape", "validator:meta-shape", "validator:function-shape", "validator:for-shape", "validator:if-shape", "validator:var-shape", "validator:unsupported-statement", "validator:unsupported-expression", "validator:string-concat", "validator:semantic-loss", "validator:list-arg", "validator:wrapped-format"]>, z.ZodEnum<["off", "warn", "error"]>>;
107
119
  }, "strict", z.ZodTypeAny, {
108
120
  enabled: boolean;
109
- rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
121
+ rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "lint:shadows-dsl-function" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
110
122
  }, {
111
123
  enabled: boolean;
112
- rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
124
+ rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "lint:shadows-dsl-function" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
113
125
  }>;
114
126
  type LinterConfig = z.infer<typeof LinterConfigSchema>;
115
127
  declare const QueueConfigSchema: z.ZodObject<{
@@ -279,16 +291,39 @@ declare const UnotestConfigSchema: z.ZodObject<{
279
291
  }>>;
280
292
  browsers: z.ZodArray<z.ZodEnum<["chromium", "firefox", "webkit"]>, "many">;
281
293
  channel: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["chrome", "msedge", "chrome-beta", "chromium"]>, z.ZodEffects<z.ZodLiteral<"bundled">, null, "bundled">, z.ZodNull]>>;
282
- viewport: z.ZodObject<{
283
- width: z.ZodNumber;
284
- height: z.ZodNumber;
294
+ viewport: z.ZodOptional<z.ZodObject<{
295
+ width: z.ZodOptional<z.ZodNumber>;
296
+ height: z.ZodOptional<z.ZodNumber>;
297
+ /** CSS pixel → device pixel ratio: 2 makes a 390×844 page a 780×1688
298
+ * screenshot, the scale design mockups are usually drawn at. */
299
+ deviceScaleFactor: z.ZodOptional<z.ZodNumber>;
285
300
  }, "strict", z.ZodTypeAny, {
286
- width: number;
287
- height: number;
301
+ width?: number | undefined;
302
+ height?: number | undefined;
303
+ deviceScaleFactor?: number | undefined;
288
304
  }, {
289
- width: number;
290
- height: number;
291
- }>;
305
+ width?: number | undefined;
306
+ height?: number | undefined;
307
+ deviceScaleFactor?: number | undefined;
308
+ }>>;
309
+ /** A device preset by its Playwright name (`"iPhone 14"`, `"Pixel 7"`):
310
+ * size, scale, touch, mobile mode and user agent at once. `viewport`
311
+ * next to it overrides the preset's size. The name is checked by the
312
+ * driver when a context opens — the list is Playwright's, and this
313
+ * schema is imported where Playwright is not installed. */
314
+ device: z.ZodOptional<z.ZodString>;
315
+ /** IANA time zone of every page (`"Europe/Madrid"`): `Date#toString`,
316
+ * `Intl` and `getTimezoneOffset` answer in it. Unset → the machine's.
317
+ * `UNOTEST_TIMEZONE` overrides it. */
318
+ timezone: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
319
+ /** BCP 47 locale of every page (`"es-ES"`): `navigator.language`, `Intl`
320
+ * defaults, Accept-Language. Unset → the browser's. `UNOTEST_LOCALE`
321
+ * overrides it. */
322
+ locale: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
323
+ /** Browser contexts one test may hold at once — the run's own plus
324
+ * every `newContext()`. The next one past it is refused: a guard
325
+ * against a loop that opens a browser per iteration. Default 4. */
326
+ maxContexts: z.ZodOptional<z.ZodNumber>;
292
327
  /** How the browser is started: a profile and extra arguments
293
328
  * (`config/launch.ts`). Unset → as before: a fresh browser, a fresh
294
329
  * context per scenario. */
@@ -338,6 +373,11 @@ declare const UnotestConfigSchema: z.ZodObject<{
338
373
  days: number;
339
374
  }>;
340
375
  storageDir: z.ZodString;
376
+ /** What page.html keeps. `redacted`: a script's content and a hidden
377
+ * field's value become a label with their length. `full`: both as the
378
+ * page had them — an app's bootstrap data and request-forgery tokens
379
+ * included. A secret field's value is hidden either way. */
380
+ pageHtml: z.ZodEnum<["redacted", "full"]>;
341
381
  }, "strict", z.ZodTypeAny, {
342
382
  tier1: true;
343
383
  tier2: boolean;
@@ -350,6 +390,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
350
390
  days: number;
351
391
  };
352
392
  storageDir: string;
393
+ pageHtml: "redacted" | "full";
353
394
  }, {
354
395
  tier2: boolean;
355
396
  tier3: {
@@ -361,6 +402,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
361
402
  days: number;
362
403
  };
363
404
  storageDir: string;
405
+ pageHtml: "redacted" | "full";
364
406
  tier1?: true | undefined;
365
407
  }>;
366
408
  dialogPolicy: z.ZodEnum<["accept", "dismiss", "manual"]>;
@@ -384,6 +426,12 @@ declare const UnotestConfigSchema: z.ZodObject<{
384
426
  * slow-but-correct navigation.
385
427
  * Override via `UNOTEST_DEFAULT_NAVIGATION_TIMEOUT_MS`. */
386
428
  defaultNavigationTimeoutMs: z.ZodNumber;
429
+ /** Wall-clock ceiling for a run's rollback pass — every `teardown(...)`
430
+ * block together. Separate from the run's own time budget on purpose: a
431
+ * test that failed BY that budget still has to give the stand back.
432
+ * Unset → 60000. A pass cut by it leaves the rest of the stand as the
433
+ * test set it, and the run result says which blocks did not finish. */
434
+ teardownTimeoutMs: z.ZodOptional<z.ZodNumber>;
387
435
  /** Cross-environment declarations (`.env.<name>` overlays). */
388
436
  env: z.ZodObject<{
389
437
  /** Variable names whose base `.env` value is deliberately shared by
@@ -397,13 +445,13 @@ declare const UnotestConfigSchema: z.ZodObject<{
397
445
  }>;
398
446
  linter: z.ZodObject<{
399
447
  enabled: z.ZodBoolean;
400
- rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "lint:goto-concat", "lint:regex-invalid", "lint:echo-assert", "lint:api-call-file-body", "lint:lint-ok-missing-reason", "lint:flow-returns-value", "lint:legacy-layout", "lint:external-variable-undeclared", "lint:env-host-from-base", "lint:one-test-per-file", "validator:unknown-function", "validator:arity", "validator:arg-kind", "validator:step-coverage", "validator:step-shape", "validator:meta-shape", "validator:function-shape", "validator:for-shape", "validator:if-shape", "validator:var-shape", "validator:unsupported-statement", "validator:unsupported-expression", "validator:string-concat", "validator:semantic-loss", "validator:list-arg", "validator:wrapped-format"]>, z.ZodEnum<["off", "warn", "error"]>>;
448
+ rules: z.ZodRecord<z.ZodEnum<["lint:deep-css", "lint:xpath", "lint:obfuscated-class", "lint:pause-explicit", "lint:disambig-by-index", "lint:evaluate-discouraged", "lint:scenario-in-root", "lint:mustache-in-dsl", "lint:goto-concat", "lint:regex-invalid", "lint:echo-assert", "lint:api-call-file-body", "lint:lint-ok-missing-reason", "lint:flow-returns-value", "lint:legacy-layout", "lint:external-variable-undeclared", "lint:env-host-from-base", "lint:one-test-per-file", "lint:shadows-dsl-function", "validator:unknown-function", "validator:arity", "validator:arg-kind", "validator:step-coverage", "validator:step-shape", "validator:meta-shape", "validator:function-shape", "validator:for-shape", "validator:if-shape", "validator:var-shape", "validator:unsupported-statement", "validator:unsupported-expression", "validator:string-concat", "validator:semantic-loss", "validator:list-arg", "validator:wrapped-format"]>, z.ZodEnum<["off", "warn", "error"]>>;
401
449
  }, "strict", z.ZodTypeAny, {
402
450
  enabled: boolean;
403
- rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
451
+ rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "lint:shadows-dsl-function" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
404
452
  }, {
405
453
  enabled: boolean;
406
- rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
454
+ rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "lint:shadows-dsl-function" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
407
455
  }>;
408
456
  mcp: z.ZodObject<{
409
457
  transport: z.ZodLiteral<"stdio">;
@@ -499,10 +547,6 @@ declare const UnotestConfigSchema: z.ZodObject<{
499
547
  defaultTimeoutMs: number;
500
548
  defaultNavigationTimeoutMs: number;
501
549
  browsers: ("chromium" | "firefox" | "webkit")[];
502
- viewport: {
503
- width: number;
504
- height: number;
505
- };
506
550
  retry: {
507
551
  count: number;
508
552
  on: ("network" | "transient" | "crash")[];
@@ -519,6 +563,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
519
563
  days: number;
520
564
  };
521
565
  storageDir: string;
566
+ pageHtml: "redacted" | "full";
522
567
  };
523
568
  dialogPolicy: "accept" | "dismiss" | "manual";
524
569
  testDir: string;
@@ -528,7 +573,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
528
573
  };
529
574
  linter: {
530
575
  enabled: boolean;
531
- rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
576
+ rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "lint:shadows-dsl-function" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
532
577
  };
533
578
  mcp: {
534
579
  transport: "stdio";
@@ -551,6 +596,8 @@ declare const UnotestConfigSchema: z.ZodObject<{
551
596
  apiBaseUrl?: string | undefined;
552
597
  uploadDir?: string | undefined;
553
598
  };
599
+ locale?: string | undefined;
600
+ timezone?: string | undefined;
554
601
  baseUrl?: string | undefined;
555
602
  webServer?: {
556
603
  url: string;
@@ -559,19 +606,23 @@ declare const UnotestConfigSchema: z.ZodObject<{
559
606
  reuseExistingServer: boolean;
560
607
  } | undefined;
561
608
  channel?: "chrome" | "msedge" | "chrome-beta" | "chromium" | null | undefined;
609
+ viewport?: {
610
+ width?: number | undefined;
611
+ height?: number | undefined;
612
+ deviceScaleFactor?: number | undefined;
613
+ } | undefined;
614
+ device?: string | undefined;
615
+ maxContexts?: number | undefined;
562
616
  launch?: {
563
617
  args?: string[] | undefined;
564
618
  userDataDir?: string | undefined;
565
619
  } | undefined;
566
620
  storageState?: string | undefined;
621
+ teardownTimeoutMs?: number | undefined;
567
622
  }, {
568
623
  defaultTimeoutMs: number;
569
624
  defaultNavigationTimeoutMs: number;
570
625
  browsers: ("chromium" | "firefox" | "webkit")[];
571
- viewport: {
572
- width: number;
573
- height: number;
574
- };
575
626
  retry: {
576
627
  count: number;
577
628
  on: ("network" | "transient" | "crash")[];
@@ -587,6 +638,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
587
638
  days: number;
588
639
  };
589
640
  storageDir: string;
641
+ pageHtml: "redacted" | "full";
590
642
  tier1?: true | undefined;
591
643
  };
592
644
  dialogPolicy: "accept" | "dismiss" | "manual";
@@ -597,7 +649,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
597
649
  };
598
650
  linter: {
599
651
  enabled: boolean;
600
- rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
652
+ rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:env-host-from-base" | "lint:one-test-per-file" | "lint:shadows-dsl-function" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
601
653
  };
602
654
  mcp: {
603
655
  transport: "stdio";
@@ -614,6 +666,8 @@ declare const UnotestConfigSchema: z.ZodObject<{
614
666
  apiBaseUrl?: string | undefined;
615
667
  uploadDir?: string | undefined;
616
668
  };
669
+ locale?: string | undefined;
670
+ timezone?: string | undefined;
617
671
  baseUrl?: string | undefined;
618
672
  webServer?: {
619
673
  url: string;
@@ -622,11 +676,19 @@ declare const UnotestConfigSchema: z.ZodObject<{
622
676
  reuseExistingServer?: boolean | undefined;
623
677
  } | undefined;
624
678
  channel?: "chrome" | "msedge" | "chrome-beta" | "chromium" | "bundled" | null | undefined;
679
+ viewport?: {
680
+ width?: number | undefined;
681
+ height?: number | undefined;
682
+ deviceScaleFactor?: number | undefined;
683
+ } | undefined;
684
+ device?: string | undefined;
685
+ maxContexts?: number | undefined;
625
686
  launch?: {
626
687
  args?: string[] | undefined;
627
688
  userDataDir?: string | undefined;
628
689
  } | undefined;
629
690
  storageState?: string | undefined;
691
+ teardownTimeoutMs?: number | undefined;
630
692
  schedules?: {
631
693
  collection: string;
632
694
  cron: string;