@unotest/web 0.47.0 → 0.49.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.
@@ -313,6 +313,10 @@ Then verify:
313
313
  run_test { name: "<scenario-name>" }
314
314
  ```
315
315
 
316
+ In a file with several `test_*`, add the one to run:
317
+ `run_test { name: "<scenario-name>", test: "test_<name>" }` — without it
318
+ the reply is `status: "test_required"` and its `next` is that call.
319
+
316
320
  `run_test` BLOCKS until the run settles — do NOT poll
317
321
  `inspect_runtime` to wait for completion (it stays a diagnostic tool
318
322
  for paused runs). The response carries `next.outcome`. **The scenario
@@ -465,8 +469,9 @@ Escape hatch:
465
469
  this: none → nothing, exactly one → the value itself, **with two or
466
470
  more extra arguments the body receives ONE array** — destructure it:
467
471
  `evaluate('function(n){ return n * 2 }', 21)` and
468
- `evaluate('([a, b]) => a + b', a, b)`. The linter's warning says so
469
- when it sees two or more.
472
+ `evaluate('([a, b]) => a + b', a, b)`. One argument is `(n) =>`,
473
+ never `([n]) =>` — that fails with `2 is not iterable`. The linter's
474
+ warning states the rule whenever it sees one or more.
470
475
  - Native dialogs (`confirm()` / `alert()` / `prompt()`) are accepted by
471
476
  the runner itself (`dialogPolicy: "accept"` by default) — a "Remove"
472
477
  that asks first needs no special step.
@@ -444,6 +444,10 @@ Then verify:
444
444
  run_test { name: "<scenario-name>" }
445
445
  ```
446
446
 
447
+ In a file with several `test_*`, add the one to run:
448
+ `run_test { name: "<scenario-name>", test: "test_<name>" }` — without it
449
+ the reply is `status: "test_required"` and its `next` is that call.
450
+
447
451
  `run_test` BLOCKS until the run settles — do NOT poll
448
452
  `inspect_runtime` to wait for completion (it stays a diagnostic tool
449
453
  for paused runs). The response carries `next.outcome`. **The scenario
@@ -596,8 +600,9 @@ Escape hatch:
596
600
  this: none → nothing, exactly one → the value itself, **with two or
597
601
  more extra arguments the body receives ONE array** — destructure it:
598
602
  `evaluate('function(n){ return n * 2 }', 21)` and
599
- `evaluate('([a, b]) => a + b', a, b)`. The linter's warning says so
600
- when it sees two or more.
603
+ `evaluate('([a, b]) => a + b', a, b)`. One argument is `(n) =>`,
604
+ never `([n]) =>` — that fails with `2 is not iterable`. The linter's
605
+ warning states the rule whenever it sees one or more.
601
606
  - Native dialogs (`confirm()` / `alert()` / `prompt()`) are accepted by
602
607
  the runner itself (`dialogPolicy: "accept"` by default) — a "Remove"
603
608
  that asks first needs no special step.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,159 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.49.0] - 2026-10-09
4
+
5
+ ### Minor Changes
6
+
7
+ - 3531ce6: A collection can set the browser window of its scenarios.
8
+
9
+ - `viewport:` in a collection's YAML: `null` for the real browser window, `{width, height}` for an emulated size. It replaces the project's `viewport` for every scenario the collection runs; the same scenario run on its own keeps the project's window. It works with a browser profile, where `setViewport(null)` is refused.
10
+ - A wrong value is refused with what to write instead. `viewport: null` over a project `device` is refused before the first scenario starts, as in the config.
11
+ - The `setViewport(null)` refusal on a browser profile now points at the collection field.
12
+ - The viewer's collection editor keeps a hand-written `viewport:` and `prepare:` when it saves a collection.
13
+
14
+ - 78e783a: A `generate` command keeps the scenarios' copy of application values in step with the source.
15
+
16
+ - `generate` in the config: a shell command that writes `unotest/generated/` from your application's source (constants, label tables, schemas). The scenarios read the files with `readJson()`. It runs from the project root before every local `e2e` and `collection` (once for the suite) before `bundle push`, and in `box create` before the box is rented, so an edit of the source reaches the next run and the bundle carries a fresh snapshot.
17
+ - A box never runs it: it has your suite, not your application, and the snapshot arrived in the bundle. `lint` does not run it.
18
+ - A failure refuses the run with exit 95 and names the command. A failure in `box create` creates no box. `--no-generate` on `e2e`, `collection`, `bundle push` and `box create` skips it for one run.
19
+ - `init` adds `unotest/generated/` to the project's `.gitignore`, and `bundle push` packs the folder although git ignores it.
20
+
21
+ - bf95375: A box keeps the names it sets on every run to itself.
22
+
23
+ - `env set` and the values page refuse a value named `UNOTEST_RUN_ID`, `UNOTEST_ENV`, `UNOTEST_BUNDLE_ID`, `UNOTEST_EDITOR_TYPES`, `UNOTEST_BOX_SECRET_NAMES`, `UNOTEST_QUEUE_LEASE`, `UNOTEST_RUN_TRIGGER` or `UNOTEST_RUN_ACTOR`, naming it. Before, such a value was taken and won over the box's own: `UNOTEST_BUNDLE_ID=""` made a box run look like a local one and let it run `generate`.
24
+ - A value already stored under one of these names stays listed on the box, and no run receives it. Remove it with `env rm`.
25
+ - Other `UNOTEST_*` names are set as before, `UNOTEST_BROWSER_CHANNEL` included.
26
+
27
+ - a262cb0: A box runs and reports each test of a file of several `test_*` on its own.
28
+
29
+ - Run again on one test in a box's viewer runs that test only. A box older than this runs the whole file.
30
+ - Run notifications (Slack, Telegram, webhook) and GitHub checks count tests, not files: `smoke failed — 1 of 42 tests`, and a failed test of a file of several is named `file › test`. The webhook fields keep their names: `scenariosTotal`, `scenariosFailed` and `failedScenarios` now count and name tests.
31
+ - `bundle push --run --follow` prints each test of such a file between the file's start and end, with its own failed steps, `log` and `note` lines, under `file › test`.
32
+ - A test that skipped itself prints in `--follow` as it does in a local run, `○ file › test — skipped: <reason>`, not as passed. A client older than this still prints it as passed.
33
+
34
+ - 970813c: A scenario file can hold several `test_*` functions, and each one is a test of its own: its own run, browser context, retry and verdict, with its own failure bundle, screenshots and console. Functions of the file are shared between its tests; state is not. `npx @unotest/web e2e <file>` plays them in order and exits 1 when any of them failed. A file with one test runs exactly as before. `e2e <file> --test <name>` plays one test, named as the function is or without its `test_` prefix. A name the file does not have is refused with exit 2 and the list of its tests.
35
+
36
+ `// @expect-fail` can mark one test: put it in the `//` comments directly above that test's `function` line, with no blank line between. A marker in the file's leading comments, set apart from the first function by a blank line, still marks every test of the file.
37
+
38
+ The MCP tool `run_test` takes `test` too. A debug session follows one test, so for a file that holds several, `run_test` without `test` starts nothing and answers with the tests and the call to make:
39
+
40
+ ```json
41
+ {
42
+ "status": "test_required",
43
+ "reason": "auth/login holds 3 tests; run_test plays one — name it in `test`.",
44
+ "tests": ["test_login_ok", "test_login_locked", "test_logout"],
45
+ "next": "run_test {name: \"auth/login\", test: \"test_login_ok\"}"
46
+ }
47
+ ```
48
+
49
+ An unknown `test` answers `status: "test_not_found"` with the same list.
50
+
51
+ A collection counts tests, not files. A file of three tests is three verdicts: its line in the `collection` output says `✗ <file> — 1 of 3 failed` and lists each test under it, and the summary counts passed, failed and skipped tests. A file that never started (bailed or busy) is still counted as one file.
52
+
53
+ The viewer shows each test on its own. A file of several tests is a node in the scenarios tree with one row per test, and the chevron opens it from the keyboard too (ArrowRight / ArrowLeft). The file's tab shows the steps of every test under its name. The file's dot and status sum its tests up: failed when one failed, skipped only when all of them skipped, failed on purpose only when every test that ran was expected to fail, otherwise passed. History shows each test's run under the file's run, and can be filtered to one test. A test's run tab shows its own steps, screenshots, console and failure, titled `file › test`, and Run again runs that test only. Home and a collection run's summary count tests, and the Home card says "tests". A link to a test's run opens that test, with its name and status in the tab.
54
+
55
+ `lint:one-test-per-file` no longer reports anything: several `test_*` in one file is now the way to write a suite of related tests.
56
+
57
+ A test that skips itself (`skip(reason)`) now shows as **skipped**, not as passed. The run tree, the history strip, the run tab (its progress bar included) and the overview draw it grey with its reason, the overview gets a "skipped" card when any test's last run skipped and no longer calls such a field "Everything is green", and a collection row counts skipped tests apart from passed ones. A run still in progress that ends in a skip turns skipped at once, without a reload. `box run` prints `(skipped)` with the reason, and its `--json` answer carries `skipReason`. Like a local `e2e`, it exits 0 on a skip.
58
+
59
+ **Migration:** `RunStatus` has a new value, `"skipped"`, and `RunMeta` and the scenario index entry carry the reason (`skipReason`, `lastSkipReason`). Code that switches over `RunStatus` exhaustively must handle the new value. Code that treated `"passed"` as "finished without failing" should accept `"skipped"` as well. The run index schema version moves to 3: the viewer discards an older index on start and rebuilds it from the runs on disk. Past skipped runs then show as skipped too.
60
+
61
+ **Migration:** the run journal has two new event kinds, `file-run:test-started` and `file-run:test-finished`, written by the run of a file with several tests. Code that switches over journal events exhaustively must handle them. Runs, manifests and `run:started` carry `test` (the function name). Its tests point at the file's run through `parentRunId`, as a collection's scenarios point at the collection's.
62
+
63
+ **Migration:** `lint:one-test-per-file` is deprecated and does nothing. A config that sets it (`"lint:one-test-per-file": "error"`) still loads; remove the entry when convenient. The rule id goes away in the next major version.
64
+
65
+ ### Patch Changes
66
+
67
+ - aac77aa: A failed step now brings its notes and the whole diff along.
68
+
69
+ - A failed statement carries the `note(label, value)` calls of its `step(...)` block: printed under the error in the `e2e` output as `note label: value`, and returned as `lastFailure.notes` (the `error` of a `run_test` reply). Secrets are masked as in the run journal. A nested step reports its own notes; a step that noted nothing reports an empty list.
70
+ - `assertEqual` shows every line of its diff. The `… N unchanged …` folding is gone from its failure message.
71
+
72
+ - 269cc10: `filter({...})` refuses a key it does not read.
73
+
74
+ - It reads `hasText`, `hasNotText`, `has` and `hasNot`. Any other key (`hasClass`, `visible`, a typo of `hasText`) used to be dropped without a word, so `count(getByRole('row').filter({visible: true}))` counted every row and the check passed.
75
+ - `lint` reports it as `lint:filter-unknown-option` (an error), and the run fails the step with the same message: the key, and the keys `filter` takes. `visible` is not supported yet.
76
+ - The MCP structured locator refuses such a `filter` step too, instead of stripping the key.
77
+
78
+ **Migration:** a scenario with an unknown or misspelled key in `filter` that used to pass now fails, at lint and at run time — on purpose: it was checking less than it said. Use one of the four keys, or a CSS `locator('tr.done')` for a class or an attribute of the element itself.
79
+
80
+ - e54db1a: Quieter linter on two rules.
81
+
82
+ - `lint:echo-assert` no longer fires when the text you fill is only part of the text you wait for. `fill(box, 'helper')` followed by `waitForText('ann/helper')` is not an echo: the page must show more than was typed. It still warns when the waited text is part of the filled value, equality included.
83
+ - `lint:evaluate-discouraged` gives one warning per file instead of one per call. The warning sits on the first call and says how many calls the file has. A call under `// lint-ok: <reason>` is left out of the count; with every call silenced, there is no warning. The argument hints (one value vs one array) name the lines they apply to.
84
+
85
+ - Updated dependencies [3531ce6]
86
+ - Updated dependencies [aac77aa]
87
+ - Updated dependencies [78e783a]
88
+ - Updated dependencies [970813c]
89
+ - @unotest/protocol@0.49.0
90
+ - @unotest/viewer@0.49.0
91
+ - @unotest/core@0.49.0
92
+ - @unotest/dsl@0.49.0
93
+ - @unotest/grounder-client@0.49.0
94
+
95
+ ## [0.48.0] - 2026-10-08
96
+
97
+ ### Minor Changes
98
+
99
+ - 2c63fd2: The viewer's status bar on a box now labels both versions — `suite 0.47.0 · box 0.48.0` — instead of a bare `v0.47.0` that read as the box's version while it was the suite's `@unotest/web`. When the suite runs an `@unotest/web` older than the lowest version the box runs suites on, the bar shows a yellow "suite too old" mark; hovering it says which version to raise in `unotest/package.json` before pushing the suite again. A viewer outside a box shows its version as before.
100
+
101
+ The box publishes its own version and that lowest supported `@unotest/web` to each environment's viewer. A box older than this change publishes neither, and the bar then labels the suite's version alone.
102
+
103
+ - f4e2a0c: New command `bundle rm <bundle-id>`: the project token that pushes a bundle can now take it back — one pushed with the wrong project's token, or one nobody wants. The box looks for the bundle in the token's project only and answers "not found in project …" the same way whether another project holds it or none does, so a token learns nothing about other projects.
104
+
105
+ A bundle in use stays: one an environment runs, or one a waiting or running ticket needs (a queued run, or a switch on its way to it), is refused with the environments named. `--switch-to <bundle-id>` first switches the environments that run it to another bundle of the same project, the way the viewer does; the box answers as the switches start, and the same `bundle rm` removes the bundle once they have landed. An environment that follows its branch cannot be switched by hand: push a newer bundle to that branch first.
106
+
107
+ A box older than bundle removal refuses and removes nothing: update the box, then retry. `unotest-web --help` now also says that `env push` merges into the box's values and lists `--prune`.
108
+
109
+ - 57beda5: `env push` now merges an environment's values into the box instead of replacing them. Every name your files carry is set; a name the box holds that your files do not carry stays there and is listed as kept, so a value put on the box with `env set` or on the admin page survives a push. A name that moved between your `.env*` and `.secrets*` files moves on the box too and is printed as `NAME: variable → secret`. `--dry-run` says that it cannot know which names the box holds.
110
+
111
+ New flag `--prune`: also remove what the box holds and your files do not, the target override included, and list it. `UNOTEST_*` names on the box are never pruned, because a push never sends them.
112
+
113
+ A box too old to merge refuses the push with "only replaces an environment's values" and changes nothing: update the box, then retry. The CLI never falls back to a replace.
114
+
115
+ **Migration:** this changes what a published command does. A push used to remove every name its files did not carry; it now keeps them. To get the old behaviour, add `--prune`: `npx @unotest/web env push <env> --prune`. With `--json`, the answer's `status` is now `"merged"`, and it carries `set`, `kept`, `removed` and `moved`.
116
+
117
+ - 0368c9c: `bundle push --run --follow` now prints what each run printed while it goes, so a CI log says why a suite failed, not only that it did: every scenario's start and end, each failed step with where it is and its error, and the run's `log` and `note` lines. With several collections each line starts with `[collection]`; with `--json` the lines go to stderr and stdout stays the one JSON document. Each line is printed once, also when the box did not answer for a moment. Exit codes are unchanged.
118
+
119
+ The box reads the lines off the run's own journal and gives them only to the token that ordered the run, the same rule as the run's state. Before they leave the box it masks the environment's secrets again (`‹secret:NAME›`), including a value the run itself did not know to mask: a CI log of a public repository is read by everybody.
120
+
121
+ A box too old to give lines says so once — "this box does not stream run lines; the verdict and link follow" — and the follow ends as before.
122
+
123
+ - 5c9b1f1: Scenarios can negate with `!x` and append to an array with `arr.push(x)`.
124
+
125
+ `!x` works as in JavaScript: it returns `true` or `false`, treats `0`, `""`, `null` and `false` as falsy, the same values `if` treats as false, and binds tighter than `&&` and `||`. Negations no longer need `x == false`.
126
+
127
+ `arr.push(x, …)` appends to the array in place and returns its new length, so a loop can collect values in one array. A variable holds the array itself, not a copy: after `b = a; a.push(1)`, `b` sees the new element, and a helper that pushes onto an array it was given changes the caller's array. Calling `push` on a value that is not an array fails the step with the value's type in the message, and the linter flags `push` on a string. Other array methods are still not available, and the "arrays are literals" hint for `push` is gone.
128
+
129
+ - c54285f: `setViewport(null)` runs a test on the real browser window, as Playwright's `viewport: null` does, without a separate run on its own config. It must come before the test does anything in its browser: the first navigation, `mockRoute`, a cookie or storage value, the page clock, `emulateMedia`, a second page or context. The browser context is opened again with the same options, so locale, storage state and the rest stay as configured. After any of those calls it is refused with that rule, because the new context would lose them. On a browser profile (`launch.userDataDir`, `launch.profile`) it is always refused; `viewport: null` in the config remains the way to run a whole suite on the real window.
130
+
131
+ ### Patch Changes
132
+
133
+ - db5c5a5: `assertCount`, `assertVisible`, `assertHidden` and `assertValue` accept their options object in a scenario: `assertCount(loc, 3, {timeout: 8000})` (or `timeoutMs`) now passes `lint` and the `run_test` check. The runner already honoured the option, but the validator refused the extra argument. A misspelled key is still refused with the list of supported ones.
134
+ - a160836: A box operator can now add an environment to a project without editing the box configuration and without restarting the daemon, from a shell on the box: `docker compose exec boxd node /boxd.mjs environment add <project> <name> --target <url>`, with optional `--default-branch`, `--no-switch`, `--concurrency` and `--by`. `environment list` (the same way) shows every environment and where it came from (the configuration, added on the box, or a pull request's preview), and `environment remove` takes back one added this way — never one from the configuration, and not while a run waits or runs in it; its run history and values stay on disk. The box serves an added environment on its next sweep, or as soon as a push names it — but the box looks for new environments on a push at most once every 10 seconds, so a push refused for a missing environment in the last 10 seconds is refused again until they are up. A name the configuration also declares is refused, and if the configuration declares it later, the configuration wins.
135
+
136
+ When `env push` or `bundle push --run` names an environment the box does not have, the box now says which environments the token's project has, and the CLI lists them with the command a box operator runs to add the missing one.
137
+
138
+ - 1384e6f: The note `bundle push` prints when the box has no value for a name now lists only the names the suite actually reads — in its scenarios and in `_helpers/` — instead of every name declared in `unotest/.env.example`. A name read as a bare identifier or as an `apiCall` base is said to stop a run at the first step that reads it; a name read with `env()` is said to give `null` there. A suite that computes a name at run time (`env(someVariable)`) still gets the full declared list with the conditional wording, since its reads cannot be known before it runs. When the suite reads none of the missing names, there is no note.
139
+ - 88f7b2f: The `evaluate` warning now also states how a single extra argument arrives: as the value itself, so the body is written `(x) => …`, not `([x]) => …` (which fails with "is not iterable"). The DSL reference states the whole rule for `evaluate`, `evaluateOnLocator` and `evaluateInServiceWorker`: no extra argument gives `undefined`, one gives the value itself, two or more give one array. Behaviour is unchanged.
140
+ - 4c4af51: The lint that runs before a scenario now honours `linter.rules` from your config, the same as `unotest-web lint`. A rule set to `off` (`lint:disambig-by-index` is off by default) no longer prints on `e2e` or `run_test`, and a rule set to `warn` or `error` is reported at that level. `run_test` refuses to start on a rule your config raises to `error`. `e2e` prints it as an error and still runs the scenario.
141
+ - 07d39d3: The `lint:one-test-per-file` warning now shows the replacement: each former test becomes a `step.soft(...)` of one `test_*`, or moves to a file of its own. It also links the new guide "Port a @playwright/test spec" (https://docs.unotest.com/guides/port-a-spec/), which ports a spec with several tests and runs one test over cases from a table or a JSONL file.
142
+ - 2d35731: `unotest-web` now warns when it is not the copy your suite installed. Run as `npx @unotest/web lint` from the project root, where no package sits, it used to take whatever version npx had cached without a word. It now prints one line with both versions and their paths, plus the command to run it from the suite instead (`cd unotest && npx unotest-web …`). When nothing is installed for the suite, the line asks you to run `npm i` in `unotest/`. The run goes on in both cases. From inside `unotest/`, wherever the suite resolves to this same copy, or when the copy that runs is the same version the suite installed, nothing is printed.
143
+ - f9100af: `wait(ms)` and `waitForTimeout(ms)` in a scenario are now answered with `use pause(ms) for a fixed delay, or a waitFor* for a state`, as `sleep` already was. The hint now appears on the run too, not only in `lint`: a scenario that reaches the runner with an unknown function name fails with `Unknown DSL function "wait" at 3:5 — use pause(ms) …` when the name is a known habit, and with the bare error otherwise.
144
+ - Updated dependencies [2c63fd2]
145
+ - Updated dependencies [a160836]
146
+ - Updated dependencies [f4e2a0c]
147
+ - Updated dependencies [57beda5]
148
+ - Updated dependencies [0368c9c]
149
+ - Updated dependencies [5c9b1f1]
150
+ - Updated dependencies [f9100af]
151
+ - @unotest/viewer@0.48.0
152
+ - @unotest/protocol@0.48.0
153
+ - @unotest/dsl@0.48.0
154
+ - @unotest/core@0.48.0
155
+ - @unotest/grounder-client@0.48.0
156
+
3
157
  ## [0.47.0] - 2026-10-07
4
158
 
5
159
  ### Minor Changes
@@ -1570,7 +1724,7 @@ picker on` fetches this project's own access and writes the three grounder
1570
1724
 
1571
1725
  That screen named one command:
1572
1726
 
1573
- npx @unotest/web bundle push --run --env ENVIRONMENT
1727
+ npx @unotest/web bundle push --run --env ENVIRONMENT
1574
1728
 
1575
1729
  In a project that has just been created, the command refuses with `no box
1576
1730
  address` before it does anything: the project holds neither the box's address
@@ -1579,8 +1733,8 @@ address` before it does anything: the project holds neither the box's address
1579
1733
 
1580
1734
  The screen now names both steps, in the order they have to happen:
1581
1735
 
1582
- npx @unotest/web box access larch
1583
- npx @unotest/web bundle push --run --env ENVIRONMENT
1736
+ npx @unotest/web box access larch
1737
+ npx @unotest/web bundle push --run --env ENVIRONMENT
1584
1738
 
1585
1739
  The box's own name is filled in when its licence carries one. `box access`
1586
1740
  with no name refuses, and its refusal offers to create a box — to somebody who
@@ -2953,6 +3107,8 @@ KEY | CREDENTIAL` is sent empty and flagged `secret`, and `GET /api/targets`
2953
3107
  outcome. The run journal gains `step-block:started` / `step-block:finished`
2954
3108
  envelopes per block (label, tag, soft, outcome, whole-block duration).
2955
3109
  Editor typings declare the new forms.
3110
+ A worked example — cases from a table and from a JSONL file, each a
3111
+ tagged soft step: [Port a @playwright/test spec](https://docs.unotest.com/guides/port-a-spec/).
2956
3112
  - ad7c918: The run journal records every loop pass (`loop:iteration`, with the loop
2957
3113
  statement's position and the pass index), which the viewer's new Trace
2958
3114
  view uses to show a loop as one group per iteration. Also: when an error
package/README.md CHANGED
@@ -352,7 +352,8 @@ npx @unotest/web box run checkout-mqf3pwr1 --env acme/test
352
352
  ```
353
353
 
354
354
  `box run` prints the failure, the results of soft steps and the judge's
355
- verdicts. When you want the artifacts too:
355
+ verdicts. A run whose test skipped itself reads `skipped`, with the reason,
356
+ and exits 0 like a local skip. When you want the artifacts too:
356
357
 
357
358
  ```sh
358
359
  npx @unotest/web box run checkout-mqf3pwr1 --env acme/test --download