@unotest/web 0.29.0 → 0.31.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 +53 -2
- package/.claude/skills/write-e2e-test-ground/SKILL.md +56 -2
- package/CHANGELOG.md +396 -0
- package/bin/unotest-web.js +3 -1
- package/dist/config/schema.d.ts +21 -21
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +12 -4
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +34 -4
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.d.ts +22 -2
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +1 -1
- package/dist/{interfaces-SzdtOj1F.d.ts → interfaces-B34N4rfI.d.ts} +17 -2
- package/dist/legacy-layout-By62PkbP.d.ts +8 -0
- package/dist/linter-Yf4gPe5J.d.ts +36 -0
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.d.ts +13 -2
- 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 +43 -2
- package/guides/dsl-reference.md +156 -9
- package/package.json +8 -8
- package/src/mcp/prompts/agent-test-author.md +11 -1
- package/types/unotest-dsl.d.ts +31 -7
- package/dist/linter-CM2CpIyW.d.ts +0 -21
|
@@ -87,6 +87,15 @@ APP_BASE_URL) — same idea as run_test's per-call `env`.
|
|
|
87
87
|
`explore_start` opens the browser itself. Do NOT call `new_context`
|
|
88
88
|
first — it's for RESETTING the browser (Phase 5), not for starting.
|
|
89
89
|
|
|
90
|
+
**Variables in steps.** In a value field (`fill` value, `press` key,
|
|
91
|
+
`select_option` value, `goto` url) the bare NAME of a `.env`/`.secrets`
|
|
92
|
+
variable is a reference: `value: "PASSWORD"` types the secret live and
|
|
93
|
+
the saved test says `fill(loc, PASSWORD)`. In a locator's text or an
|
|
94
|
+
assertion's text a bare NAME is the literal it looks like — to reference
|
|
95
|
+
a variable there write `{{NAME}}` (`text: "{{GREETING}}"`); the reply
|
|
96
|
+
warns when a literal equals a variable's name. Never type a secret's
|
|
97
|
+
VALUE: it is recorded as the name anyway, and the reply says so.
|
|
98
|
+
|
|
90
99
|
### Phase 2 — first recorded step: navigate
|
|
91
100
|
|
|
92
101
|
```
|
|
@@ -418,7 +427,23 @@ Assertions:
|
|
|
418
427
|
|
|
419
428
|
State reads:
|
|
420
429
|
- `getTitle()`, `getUrl()`, `getAttribute(loc, name)`, `getInnerText(loc)`,
|
|
421
|
-
`getInputValue(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'))`).
|
|
433
|
+
- A `textarea` / `input` VALUE is not page text: `waitForText` and
|
|
434
|
+
`assertText` never see it. Check it with `assertValue(loc, expected)`
|
|
435
|
+
— it polls until the timeout (5 s by default, `{timeout}` to raise
|
|
436
|
+
it), so a value that fills in late is fine. For a substring of the
|
|
437
|
+
value there is no polling assert: read it in a loop —
|
|
438
|
+
`for (i = 0; i < 20; i = i + 1) { if (textContains(getInputValue(loc), 'x')) { break; } pause(200); // lint-ok: polling a textarea value }`
|
|
439
|
+
— then assert.
|
|
440
|
+
- Only the vocabulary above is expression language. `!` is rejected by
|
|
441
|
+
the parser (`unotest-web lint`: `parse error — Invalid token`);
|
|
442
|
+
`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
|
|
446
|
+
(`assertHidden`, `assertCount`, `.filter({hasText})`, `textContains`).
|
|
422
447
|
|
|
423
448
|
Sandbox primitives:
|
|
424
449
|
- `shell("cmd", "arg", …, options?)` — `execFile` style, no shell interpretation. Non-zero exit fails the step; pass `{allowNonZero: true}` to inspect `res.code` yourself. Wall-clock budget `{timeoutMs}` (default 120s).
|
|
@@ -433,8 +458,17 @@ Sandbox primitives:
|
|
|
433
458
|
JSON — there is no key-name magic.
|
|
434
459
|
|
|
435
460
|
Escape hatch:
|
|
436
|
-
- `evaluate(\`js body\`,
|
|
461
|
+
- `evaluate(\`js body\`, arg?)` — raw backticks, no `${}`. Linter
|
|
437
462
|
warns `lint:evaluate-discouraged`. Use only when nothing above fits.
|
|
463
|
+
The body gets at most ONE argument — extra arguments reach it like
|
|
464
|
+
this: none → nothing, exactly one → the value itself, **with two or
|
|
465
|
+
more extra arguments the body receives ONE array** — destructure it:
|
|
466
|
+
`evaluate('function(n){ return n * 2 }', 21)` and
|
|
467
|
+
`evaluate('([a, b]) => a + b', a, b)`. The linter's warning says so
|
|
468
|
+
when it sees two or more.
|
|
469
|
+
- Native dialogs (`confirm()` / `alert()` / `prompt()`) are accepted by
|
|
470
|
+
the runner itself (`dialogPolicy: "accept"` by default) — a "Remove"
|
|
471
|
+
that asks first needs no special step.
|
|
438
472
|
|
|
439
473
|
Time helpers: `nowMs()`, `today()`, `daysFromNow(n)`. Marker helper: `randomWord(len)` — random lowercase letters, digit-free. Debug helper: `json(value)` — serialize any value for an assert message: `assertTrue(res.status == 202, json(res.body))`.
|
|
440
474
|
|
|
@@ -570,11 +604,23 @@ the old browser context, then `run_test` again.
|
|
|
570
604
|
| `wait_for_url` | `value` (the pattern) | `options` |
|
|
571
605
|
| `enter_frame` | `locator` | |
|
|
572
606
|
| `exit_frame` | | |
|
|
607
|
+
| `screenshot` | | `name`, `fullPage`, `locator`, `outline`, `evidenceOnly` |
|
|
573
608
|
|
|
574
609
|
When recording (`explorationId` present): `description` + `section`
|
|
575
610
|
are required. Ad-hoc (no `explorationId`): no description/section,
|
|
576
611
|
no recording.
|
|
577
612
|
|
|
613
|
+
`screenshot` is the **evidence** step: it files a PNG under the session
|
|
614
|
+
(`.unotest/explorations/<explorationId>/screenshots/<NNN>-<name>.png`,
|
|
615
|
+
`adhoc/` without a session), replies with `{path, width, height}` and
|
|
616
|
+
the image itself, and becomes `screenshot(name)` in the saved test.
|
|
617
|
+
`name` defaults to the section slug; `locator` captures one element
|
|
618
|
+
(the saved test degrades that to a page capture); `outline: true` adds
|
|
619
|
+
the page outline to the reply; `evidenceOnly: true` keeps the file but
|
|
620
|
+
records no step. `explore_stop` lists every file in `artifacts`. Take
|
|
621
|
+
one wherever a human would want to check a green run after the fact —
|
|
622
|
+
do not write throwaway scenarios just to get a picture.
|
|
623
|
+
|
|
578
624
|
## Failure modes you will hit
|
|
579
625
|
|
|
580
626
|
- **`StaleRefError`** — ref no longer in the DOM. Re-`get_page_snapshot`
|
|
@@ -635,6 +681,11 @@ Every locator must resolve to exactly one element. When you see
|
|
|
635
681
|
multi-matches.** Element order is brittle. Use `.filter({hasText:
|
|
636
682
|
'…'})` or `.filter({has: someLocator})`. Linter flags index-based
|
|
637
683
|
picking as `lint:disambig-by-index`.
|
|
684
|
+
- **Don't assert "the first row of the list / audit".** On a shared
|
|
685
|
+
stand another run (or a person) writes rows between your steps. Give
|
|
686
|
+
your own data a unique marker (`randomWord()` in a name or note),
|
|
687
|
+
find your row by it (`.filter({hasText: marker})`), and remove what
|
|
688
|
+
you created at the end of the scenario.
|
|
638
689
|
- **Don't reach for `locator(...)` with `>` combinators, hashed class
|
|
639
690
|
names, or `xpath=…`.** Stop and ask the user whether the app should
|
|
640
691
|
expose a `data-testid` or accessible name.
|
|
@@ -112,6 +112,15 @@ APP_BASE_URL) — same idea as run_test's per-call `env`.
|
|
|
112
112
|
`explore_start` opens the browser itself. Do NOT call `new_context`
|
|
113
113
|
first — it's for RESETTING the browser (Phase 5), not for starting.
|
|
114
114
|
|
|
115
|
+
**Variables in steps.** In a value field (`fill` value, `press` key,
|
|
116
|
+
`select_option` value, `goto` url) the bare NAME of a `.env`/`.secrets`
|
|
117
|
+
variable is a reference: `value: "PASSWORD"` types the secret live and
|
|
118
|
+
the saved test says `fill(loc, PASSWORD)`. In a locator's text or an
|
|
119
|
+
assertion's text a bare NAME is the literal it looks like — to reference
|
|
120
|
+
a variable there write `{{NAME}}` (`text: "{{GREETING}}"`); the reply
|
|
121
|
+
warns when a literal equals a variable's name. Never type a secret's
|
|
122
|
+
VALUE: it is recorded as the name anyway, and the reply says so.
|
|
123
|
+
|
|
115
124
|
### Phase 2 — record the known steps as ONE batch
|
|
116
125
|
|
|
117
126
|
**Batch-first default.** When the task brief already spells out the
|
|
@@ -549,7 +558,23 @@ Assertions:
|
|
|
549
558
|
|
|
550
559
|
State reads:
|
|
551
560
|
- `getTitle()`, `getUrl()`, `getAttribute(loc, name)`, `getInnerText(loc)`,
|
|
552
|
-
`getInputValue(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'))`).
|
|
564
|
+
- A `textarea` / `input` VALUE is not page text: `waitForText` and
|
|
565
|
+
`assertText` never see it. Check it with `assertValue(loc, expected)`
|
|
566
|
+
— it polls until the timeout (5 s by default, `{timeout}` to raise
|
|
567
|
+
it), so a value that fills in late is fine. For a substring of the
|
|
568
|
+
value there is no polling assert: read it in a loop —
|
|
569
|
+
`for (i = 0; i < 20; i = i + 1) { if (textContains(getInputValue(loc), 'x')) { break; } pause(200); // lint-ok: polling a textarea value }`
|
|
570
|
+
— then assert.
|
|
571
|
+
- Only the vocabulary above is expression language. `!` is rejected by
|
|
572
|
+
the parser (`unotest-web lint`: `parse error — Invalid token`);
|
|
573
|
+
`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
|
|
577
|
+
(`assertHidden`, `assertCount`, `.filter({hasText})`, `textContains`).
|
|
553
578
|
|
|
554
579
|
Sandbox primitives:
|
|
555
580
|
- `shell("cmd", "arg", …, options?)` — `execFile` style, no shell interpretation. Non-zero exit fails the step; pass `{allowNonZero: true}` to inspect `res.code` yourself. Wall-clock budget `{timeoutMs}` (default 120s).
|
|
@@ -564,8 +589,17 @@ Sandbox primitives:
|
|
|
564
589
|
JSON — there is no key-name magic.
|
|
565
590
|
|
|
566
591
|
Escape hatch:
|
|
567
|
-
- `evaluate(\`js body\`,
|
|
592
|
+
- `evaluate(\`js body\`, arg?)` — raw backticks, no `${}`. Linter
|
|
568
593
|
warns `lint:evaluate-discouraged`. Use only when nothing above fits.
|
|
594
|
+
The body gets at most ONE argument — extra arguments reach it like
|
|
595
|
+
this: none → nothing, exactly one → the value itself, **with two or
|
|
596
|
+
more extra arguments the body receives ONE array** — destructure it:
|
|
597
|
+
`evaluate('function(n){ return n * 2 }', 21)` and
|
|
598
|
+
`evaluate('([a, b]) => a + b', a, b)`. The linter's warning says so
|
|
599
|
+
when it sees two or more.
|
|
600
|
+
- Native dialogs (`confirm()` / `alert()` / `prompt()`) are accepted by
|
|
601
|
+
the runner itself (`dialogPolicy: "accept"` by default) — a "Remove"
|
|
602
|
+
that asks first needs no special step.
|
|
569
603
|
|
|
570
604
|
Time helpers: `nowMs()`, `today()`, `daysFromNow(n)`. Marker helper: `randomWord(len)` — random lowercase letters, digit-free. Debug helper: `json(value)` — serialize any value for an assert message: `assertTrue(res.status == 202, json(res.body))`.
|
|
571
605
|
|
|
@@ -701,16 +735,31 @@ the old browser context, then `run_test` again.
|
|
|
701
735
|
| `wait_for_url` | `value` (the pattern) | `options` |
|
|
702
736
|
| `enter_frame` | `locator` | |
|
|
703
737
|
| `exit_frame` | | |
|
|
738
|
+
| `screenshot` | | `name`, `fullPage`, `locator`, `outline`, `evidenceOnly` |
|
|
704
739
|
|
|
705
740
|
When recording (`explorationId` present): `description` + `section`
|
|
706
741
|
are required. Ad-hoc (no `explorationId`): no description/section,
|
|
707
742
|
no recording.
|
|
708
743
|
|
|
744
|
+
`screenshot` is the **evidence** step: it files a PNG under the session
|
|
745
|
+
(`.unotest/explorations/<explorationId>/screenshots/<NNN>-<name>.png`,
|
|
746
|
+
`adhoc/` without a session), replies with `{path, width, height}` and
|
|
747
|
+
the image itself, and becomes `screenshot(name)` in the saved test.
|
|
748
|
+
`name` defaults to the section slug; `locator` captures one element
|
|
749
|
+
(the saved test degrades that to a page capture); `outline: true` adds
|
|
750
|
+
the page outline to the reply; `evidenceOnly: true` keeps the file but
|
|
751
|
+
records no step. `explore_stop` lists every file in `artifacts`. Take
|
|
752
|
+
one wherever a human would want to check a green run after the fact —
|
|
753
|
+
do not write throwaway scenarios just to get a picture.
|
|
754
|
+
|
|
709
755
|
## Failure modes you will hit
|
|
710
756
|
|
|
711
757
|
- **`StaleRefError`** — a ref from an earlier lookup is no longer in
|
|
712
758
|
the DOM. Intent locators don't go stale (grounded per call) — retry
|
|
713
759
|
with the intent step, or re-ground via `ground_element`.
|
|
760
|
+
- **Grounder unavailable** — `explore_start` or the first intent step
|
|
761
|
+
says so; intent locators are off for the session. Use
|
|
762
|
+
`find_element({role, name, near?})` and its ref instead.
|
|
714
763
|
- **`RefResolveError`** — element has no stable identifier (testId /
|
|
715
764
|
role+name / aria-label / placeholder / alt / title / text / href /
|
|
716
765
|
stable id / name attribute). Best fix: ask the app team to add
|
|
@@ -769,6 +818,11 @@ Every locator must resolve to exactly one element. When you see
|
|
|
769
818
|
multi-matches.** Element order is brittle. Use `.filter({hasText:
|
|
770
819
|
'…'})` or `.filter({has: someLocator})`. Linter flags index-based
|
|
771
820
|
picking as `lint:disambig-by-index`.
|
|
821
|
+
- **Don't assert "the first row of the list / audit".** On a shared
|
|
822
|
+
stand another run (or a person) writes rows between your steps. Give
|
|
823
|
+
your own data a unique marker (`randomWord()` in a name or note),
|
|
824
|
+
find your row by it (`.filter({hasText: marker})`), and remove what
|
|
825
|
+
you created at the end of the scenario.
|
|
772
826
|
- **Don't reach for `locator(...)` with `>` combinators, hashed class
|
|
773
827
|
names, or `xpath=…`.** Stop and ask the user whether the app should
|
|
774
828
|
expose a `data-testid` or accessible name.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,401 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.31.0] - 2026-09-04
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b8e105b: New DSL function `note(label, value)`: attach a labelled value to the
|
|
8
|
+
current step — the question a data-driven case asked, the answer it got
|
|
9
|
+
— kept in the run journal and shown in the viewer under the step, during
|
|
10
|
+
and after the run. Any value: a string as it is, anything else as JSON;
|
|
11
|
+
cut at 4 KB (marked `truncated`), secrets masked. `log(...)` now also
|
|
12
|
+
lands in the journal under the step that wrote it (the stdout line
|
|
13
|
+
stays), and `assertJudge` verdicts carry the position of their statement,
|
|
14
|
+
so the viewer can show a verdict under its step and the whole verdict on
|
|
15
|
+
a failed step's error card. Each of these events records the DSL call's
|
|
16
|
+
`file` / `line` / `col` and the nearest entry-file statement
|
|
17
|
+
(`entryLine` / `entryCol`) — a note from inside a helper is attributed to
|
|
18
|
+
the entry step that called the helper. Without a run journal
|
|
19
|
+
(exploration, an ad-hoc runtime) `note` goes to the logger; nothing
|
|
20
|
+
fails.
|
|
21
|
+
- 56784b3: `ExecutionWalker.run` takes a trailing `entryArgs` list and binds it by
|
|
22
|
+
position onto the entry function's parameters (missing ones `null`) in
|
|
23
|
+
the root scope before the body runs — what `explore_run_flow` needs to
|
|
24
|
+
replay a parameterised `flow_*` helper live.
|
|
25
|
+
- 56784b3: `explore_run_flow` replays parameterised flows: pass `args` (positional
|
|
26
|
+
JSON values, `"{{NAME}}"` for a variable) and the helper's parameters are
|
|
27
|
+
bound for the live replay — `flow_login(username, password)` no longer
|
|
28
|
+
fails with `Variable "username" is not defined`. `explore_start` lists
|
|
29
|
+
each flow's `params`; a wrong argument count is refused with the
|
|
30
|
+
signature. The recording keeps variable NAMES, so the saved test reads
|
|
31
|
+
`flow_login(LOGIN, PASSWORD);` — `flow_call` is now a regular action
|
|
32
|
+
plugin rendering the call with its arguments.
|
|
33
|
+
- 307582f: New `explore_step` action `screenshot` — the evidence step of an
|
|
34
|
+
exploration. It files a PNG under the session
|
|
35
|
+
(`.unotest/explorations/<explorationId>/screenshots/<NNN>-<name>.png`,
|
|
36
|
+
`adhoc/` without a session), replies with the absolute `path`, `width`
|
|
37
|
+
and `height`, and returns the image itself as an MCP image block (a JPEG
|
|
38
|
+
copy when the PNG is over 1 MiB; path only when both are). `name`
|
|
39
|
+
defaults to the section slug, `fullPage` captures the whole page,
|
|
40
|
+
`locator` captures one element, `outline: true` adds the page outline
|
|
41
|
+
to the reply, `evidenceOnly: true` keeps the file but records no step.
|
|
42
|
+
A recorded screenshot becomes `screenshot(name)` in the saved test; an
|
|
43
|
+
element capture degrades to a page capture with a comment and a
|
|
44
|
+
non-blocking `DEGRADED_STEP` warning. `explore_stop` now lists the
|
|
45
|
+
session's files in `artifacts`. Driver: `DriverPage.screenshotElement`.
|
|
46
|
+
- 6b61884: New linter rule `lint:external-variable-undeclared` (default `error`): a
|
|
47
|
+
bare `UPPER_SNAKE` identifier that no layer file declares — `unotest/.env`,
|
|
48
|
+
`unotest/.secrets`, or the `.env.<name>` / `.secrets.<name>` overlay of
|
|
49
|
+
the environment `lint --env <name>` / `UNOTEST_ENV` selects — is reported
|
|
50
|
+
with the files searched, instead of failing at that statement at run time
|
|
51
|
+
after the browser is up. A name bound in the file (assigned, a parameter,
|
|
52
|
+
a loop counter) is never flagged. The shell is not a declaration: a value
|
|
53
|
+
CI exports still needs its name in `unotest/.env` (`NAME=` with no value
|
|
54
|
+
declares it; the shell's value wins at run time, as before). As an error
|
|
55
|
+
it is not silenced by `// lint-ok:`; downgrade it in `linter.rules` if a
|
|
56
|
+
project needs that.
|
|
57
|
+
|
|
58
|
+
`run_test` (MCP) and `e2e` run the same check before the browser starts,
|
|
59
|
+
against what the run will actually resolve — files and the ambient shell
|
|
60
|
+
— so a suite that lives on a shell variable keeps running; a name that
|
|
61
|
+
would have failed in the scenario anyway now fails up front (`run_test`
|
|
62
|
+
answers `lint_failed`; `e2e` prints it and proceeds, like every pre-run
|
|
63
|
+
diagnostic).
|
|
64
|
+
|
|
65
|
+
The linter also descends into operator, array and property-access
|
|
66
|
+
operands now, so a regex or `{{mustache}}` literal inside `a + b` is
|
|
67
|
+
diagnosed the same as one on its own.
|
|
68
|
+
|
|
69
|
+
- eddb1db: New linter rule `lint:one-test-per-file`: a second (third, …) top-level
|
|
70
|
+
`function test_*` in one scenario file is reported at its declaration.
|
|
71
|
+
A file is the unit a collection runs and the viewer shows — the Steps
|
|
72
|
+
tree projects a run onto the first `test_*`, so the others executed with
|
|
73
|
+
nowhere to be seen. Fold the cases into one test with tagged steps
|
|
74
|
+
(`step("…", {tag: id}, () => { … })`, `step.soft` when a case must not
|
|
75
|
+
stop the run) or move shared journeys into `flow_*` helpers under
|
|
76
|
+
`unotest/e2e/_helpers/`; `flow_*` functions in a scenario file are not
|
|
77
|
+
counted.
|
|
78
|
+
|
|
79
|
+
**Migration window:** the rule is a **warning** in 0.31 — visible in
|
|
80
|
+
`lint`, the editor and `run_test`'s warnings, silenced per line with
|
|
81
|
+
`// lint-ok: <reason>`, never blocking. A later minor release flips the
|
|
82
|
+
default to `error` (which the pre-run gate of `run_test` refuses); split
|
|
83
|
+
files that hold several `test_*` before then.
|
|
84
|
+
|
|
85
|
+
- b0a00cb: New DSL function `readJsonLine(path, filter) → object`: read a JSONL file
|
|
86
|
+
that already exists and return the first line matching the key filter —
|
|
87
|
+
no polling. `waitForJsonLine` was the only structured input into a
|
|
88
|
+
scenario, and for a fixture or a finished export its 20s timeout only
|
|
89
|
+
masked a wrong path. `readJsonLine` takes the same filter (strict equality
|
|
90
|
+
per key, dot paths for nesting, unparsable lines skipped) and fails at
|
|
91
|
+
once: `file not found: <path>` when the file is missing, `no line matches
|
|
92
|
+
{…} in <path>` with parse stats and the closest line when nothing matches.
|
|
93
|
+
Data-driven tests read their cases with it and name evidence after them
|
|
94
|
+
(`screenshot(q.id)`).
|
|
95
|
+
- 100ab9d: Data-driven steps: `step("label", {tag: q.id}, () => { … })` names the
|
|
96
|
+
case an iteration is on, and `step.soft("label", [{tag}], () => { … })`
|
|
97
|
+
records a failure inside instead of stopping the run — the rest of that
|
|
98
|
+
body is skipped, the next case runs, and the test still ends **failed**
|
|
99
|
+
with every soft failure listed (`3 soft step(s) failed: Question [q17]:
|
|
100
|
+
…`, one line per case in the CLI output; `run_test` answers
|
|
101
|
+
`next.softFailures`; `inspect_runtime`'s `lastFailure` carries `stepTag`
|
|
102
|
+
and `soft`). The failure bundle shows the page at the moment of the
|
|
103
|
+
**first** soft failure, with `soft: true` and `stepTag` in
|
|
104
|
+
`failure.json`; later soft failures live in the journal. `step.soft` is
|
|
105
|
+
allowed inside `test_*` only; nesting is free, and the outer step is a
|
|
106
|
+
group, not an assertion — a soft failure inside does not change its
|
|
107
|
+
outcome. The run journal gains `step-block:started` / `step-block:finished`
|
|
108
|
+
envelopes per block (label, tag, soft, outcome, whole-block duration).
|
|
109
|
+
Editor typings declare the new forms.
|
|
110
|
+
- ad7c918: The run journal records every loop pass (`loop:iteration`, with the loop
|
|
111
|
+
statement's position and the pass index), which the viewer's new Trace
|
|
112
|
+
view uses to show a loop as one group per iteration. Also: when an error
|
|
113
|
+
unwinds through enclosing statements, `lastFailure` (the failure bundle's
|
|
114
|
+
position, `inspect_runtime`, the CLI's `in step "…" [tag]` line) now
|
|
115
|
+
names the innermost statement that raised it — the assert inside the
|
|
116
|
+
tagged step — instead of the outermost loop that re-threw it.
|
|
117
|
+
|
|
118
|
+
### Patch Changes
|
|
119
|
+
|
|
120
|
+
- 9ae6d59: A bundle no longer declares the runner's own `UNOTEST_*` settings
|
|
121
|
+
(`UNOTEST_HEADED`, `UNOTEST_LOG_LEVEL`, …) from the suite's `.env`, so a box
|
|
122
|
+
stops warning on every push that an environment "neither sets nor holds"
|
|
123
|
+
names `env push` never sends. Both sides now share one rule
|
|
124
|
+
(`isRunnerSetting` in `@unotest/protocol`): those variables configure the
|
|
125
|
+
machine a run happens on, not the suite. A per-suite value such as the
|
|
126
|
+
navigation timeout travels with the bundle through `unotest.config.mjs`
|
|
127
|
+
(`defaultNavigationTimeoutMs`), not through `.env`.
|
|
128
|
+
- 917380d: `unotest.config.*` is merged with the defaults recursively. A partially
|
|
129
|
+
written section — `linter: { rules: { 'lint:deep-css': 'off' } }`,
|
|
130
|
+
`failureBundle: { tier3: { video: true } }`, `viewport: { width: 1920 }`
|
|
131
|
+
— now keeps the rest of that section's defaults instead of failing with
|
|
132
|
+
"config validation failed" (nested sections require all of their fields
|
|
133
|
+
at validation time, so a top-level replace made every partial section
|
|
134
|
+
invalid). Arrays and scalars still replace as a whole: `browsers:
|
|
135
|
+
['firefox']` means firefox only. The setup manual's example config,
|
|
136
|
+
which was exactly such a partial `linter`, loads as written; its
|
|
137
|
+
`timeouts` block — never a config field — is corrected to
|
|
138
|
+
`defaultTimeoutMs` / `defaultNavigationTimeoutMs`.
|
|
139
|
+
- e048334: The viewer's editor flags an undeclared external variable
|
|
140
|
+
(`lint:external-variable-undeclared`), the way `unotest-web lint` and the
|
|
141
|
+
pre-run lint already do: a bare `UPPER_SNAKE` identifier that no
|
|
142
|
+
`unotest/.env` / `.secrets` file of the active environment declares is an
|
|
143
|
+
error in the editor, naming the files searched. Protocol:
|
|
144
|
+
`DslValidateContext` gains `externalNames` / `externalSources`; the
|
|
145
|
+
viewer supplies them from the active target + environment's layer files
|
|
146
|
+
(the same files the variables panel shows) and `@unotest/web`'s language
|
|
147
|
+
service turns them into the linter's lookup.
|
|
148
|
+
- dcf4c9e: Viewer editor: lint markers appear as soon as a file opens (or the page
|
|
149
|
+
reloads), not only after the first keystroke — the editor validates the
|
|
150
|
+
source it mounted with instead of waiting for a change event that a
|
|
151
|
+
reopened tab never fires.
|
|
152
|
+
- 6646584: Docs: `hover` now lists its options (`force`, `position`, `timeout`) in
|
|
153
|
+
the DSL reference, the editor signature and the generated typings — it
|
|
154
|
+
used to read as if it took none, while `position` is exactly what a
|
|
155
|
+
hover-revealed menu at one edge of a tall element needs. The agent
|
|
156
|
+
integration guide gains "Watching a collection run": the collection's
|
|
157
|
+
`steps.jsonl` journal (path, the four `collection-run:*` events, how to
|
|
158
|
+
tail it) as the progress source for a run started in the background,
|
|
159
|
+
where the CLI's per-scenario lines only arrive when the process ends.
|
|
160
|
+
- 630d6ce: `unotest-web lint` now receives its arguments: the bin dispatcher forwarded
|
|
161
|
+
only the command name, so `lint --env <name>` linted the base environment
|
|
162
|
+
and an explicit file argument was ignored (every file was linted instead).
|
|
163
|
+
`UNOTEST_ENV=<name>` worked all along, which is why the gap went unnoticed.
|
|
164
|
+
- f3bd3cc: `lint` works on a project that has not migrated to the 0.28 layout yet.
|
|
165
|
+
A `unotest.config.*` still at the project root used to stop `lint` with
|
|
166
|
+
the same refusal `e2e` gives — right for a command that would RUN on
|
|
167
|
+
defaults, wrong for one that only grades: nobody lints a suite during
|
|
168
|
+
the very migration that touches every file. `lint` now builds its
|
|
169
|
+
context on the default settings (rule severities, helpers dir), reads
|
|
170
|
+
`unotest/.env`, `.secrets` and `_helpers/` as usual, and reports the
|
|
171
|
+
layout once as the new `lint:legacy-layout` warning (default `warn`,
|
|
172
|
+
configurable like any rule) with the migration on one line. The exit
|
|
173
|
+
code is unaffected by it. `e2e`, `collection` and `bundle push` refuse
|
|
174
|
+
exactly as before.
|
|
175
|
+
- 224d016: The MCP server says when it is running a stale build. `run_test`'s
|
|
176
|
+
pre-spawn lint, flow discovery (`explore_start`), `explore_run_flow`,
|
|
177
|
+
`generate_dsl_from_exploration` and `save_exploration_as_test` parse and
|
|
178
|
+
render DSL in the server's own process, on the modules it loaded at
|
|
179
|
+
start — after a rebuild a new DSL feature came back as a false parse
|
|
180
|
+
error until the server was restarted, while the runner child was already
|
|
181
|
+
on the new build. Those replies now carry `serverStale: true` and a
|
|
182
|
+
`serverStaleWarning` ("restart the MCP server") once the code on disk is
|
|
183
|
+
newer than the server's start; nothing changes while it is fresh.
|
|
184
|
+
- 02757c3: `serverStale` now also rides on `explore_step` / `explore_steps` (locator
|
|
185
|
+
resolution and recording run in the server's process), `find_element`
|
|
186
|
+
and `ground_element` replies — the same flag `run_test` and the
|
|
187
|
+
exploration tools already carry once the build on disk is newer than the
|
|
188
|
+
running MCP server.
|
|
189
|
+
- dcf4c9e: `goto` with a relative path resolves against the runtime's own base URL
|
|
190
|
+
instead of the browser context's. In a CLI run the two coincide; in an
|
|
191
|
+
exploration recording against an `env` override, `explore_run_flow`
|
|
192
|
+
replayed `goto('/_guard/')` on the MCP session's shared context — created
|
|
193
|
+
with the server's base — and landed on the wrong host. The replay now
|
|
194
|
+
follows the session's `baseUrl` (and its variables, as before).
|
|
195
|
+
- f3bd3cc: `screenshot(name)` accepts any string value, not only a literal. The
|
|
196
|
+
runtime always slugified whatever it received, but the validator (and
|
|
197
|
+
therefore `lint` and `run_test`) rejected `screenshot(q.id)` and
|
|
198
|
+
`screenshot(textJoin(['q-', id]))` — a data-driven test had to name every
|
|
199
|
+
capture the same. The `name` slot is now string-like, as in `fill`; a
|
|
200
|
+
non-string argument is still an `arg-kind` error.
|
|
201
|
+
- dcf4c9e: A section label reopened after another section is called out instead of
|
|
202
|
+
silently splitting a step: `explore_step` / `explore_steps` reply with
|
|
203
|
+
`sectionHint` the moment it happens, and the generated draft carries a
|
|
204
|
+
non-blocking `SPLIT_SECTION` warning naming the reopened entries. The
|
|
205
|
+
recorded order of actions is never changed; `autoRun` and `save` are not
|
|
206
|
+
held back by it.
|
|
207
|
+
- 9ae6d59: A scenario that finished in the same instant the viewer looked at it is no
|
|
208
|
+
longer recorded as `interrupted` with a finish an hour before its start.
|
|
209
|
+
The runner ends a run by appending `run:finished` and then backdating the
|
|
210
|
+
heartbeat; the viewer read those two files in the opposite order, so a
|
|
211
|
+
steps snapshot taken a few milliseconds early could meet the backdated
|
|
212
|
+
heartbeat, and the provisional verdict stuck in the run index while the
|
|
213
|
+
collection reported the scenario passed. The heartbeat is read first now.
|
|
214
|
+
A viewer restart (which rebuilds the index from disk) already corrected
|
|
215
|
+
such records; runs indexed live are right the first time.
|
|
216
|
+
- 13a2d51: Viewer, Home tab: the tile of a scenario that is running now breathes
|
|
217
|
+
through colour and shadow only — the scale pulse is gone. It moved the
|
|
218
|
+
tile's box every frame, so an automation driver never saw the tile as
|
|
219
|
+
stable and a click on it waited out its timeout. A tile that never ran
|
|
220
|
+
no longer carries `data-age="fresh"`: it has no age, so the attribute is
|
|
221
|
+
absent.
|
|
222
|
+
- 91fb562: Viewer, Home tab: clicking a scenario tile opens the scenario's LAST RUN —
|
|
223
|
+
the run whose verdict the tile's colour is showing — instead of the test
|
|
224
|
+
file. A tile that has never run still opens the test (there is no run to
|
|
225
|
+
open), and so does every tile in schedule mode, where the schedule is
|
|
226
|
+
edited from the test. The legend now reads "hover for history · click to
|
|
227
|
+
open the last run".
|
|
228
|
+
- dcf4c9e: Viewer, Home tab: the tile field no longer refits itself in a loop. The
|
|
229
|
+
header and legend take the grid's width; when the legend wrapped at one
|
|
230
|
+
width and not at the next, the field's height changed, the fit followed,
|
|
231
|
+
the width changed back — at 60 fps every tile's box moved every frame,
|
|
232
|
+
and an automation click on ANY tile ("element is not stable") waited out
|
|
233
|
+
its timeout unless forced. Header and legend now never wrap (they grow
|
|
234
|
+
past the grid instead), so the fit converges and tiles stay put. A run
|
|
235
|
+
opened from a tile is titled by its scenario, like one opened from the
|
|
236
|
+
Runs tree, instead of by its run id.
|
|
237
|
+
- dcf4c9e: Recording no longer bakes relative time or counters into a locator. An
|
|
238
|
+
accessible name such as `Run smoke — passed, 7m ago`, `just now`,
|
|
239
|
+
`yesterday`, `5 passed`, `7 runs` or a trailing badge (`Inbox · 3`,
|
|
240
|
+
`Errors: 12`) now counts as volatile: the recorder emits the stable prefix
|
|
241
|
+
(`{name: "Run smoke — passed", exact: true}`, then `/^Run smoke — passed\b/`)
|
|
242
|
+
instead of the literal, and the draft carries the `DYNAMIC_TEXT` warning
|
|
243
|
+
with that prefix. Bare numbers elsewhere stay stable (`Page 2`, `Q4 2026`,
|
|
244
|
+
`v2`, `2FA`).
|
|
245
|
+
- Updated dependencies [b8e105b]
|
|
246
|
+
- Updated dependencies [b8e105b]
|
|
247
|
+
- Updated dependencies [b8e105b]
|
|
248
|
+
- Updated dependencies [9ae6d59]
|
|
249
|
+
- Updated dependencies [56784b3]
|
|
250
|
+
- Updated dependencies [e048334]
|
|
251
|
+
- Updated dependencies [dcf4c9e]
|
|
252
|
+
- Updated dependencies [100ab9d]
|
|
253
|
+
- Updated dependencies [100ab9d]
|
|
254
|
+
- Updated dependencies [100ab9d]
|
|
255
|
+
- Updated dependencies [100ab9d]
|
|
256
|
+
- Updated dependencies [ad7c918]
|
|
257
|
+
- Updated dependencies [ad7c918]
|
|
258
|
+
- Updated dependencies [ad7c918]
|
|
259
|
+
- Updated dependencies [9ae6d59]
|
|
260
|
+
- Updated dependencies [13a2d51]
|
|
261
|
+
- Updated dependencies [91fb562]
|
|
262
|
+
- Updated dependencies [dcf4c9e]
|
|
263
|
+
- @unotest/core@0.31.0
|
|
264
|
+
- @unotest/protocol@0.31.0
|
|
265
|
+
- @unotest/viewer@0.31.0
|
|
266
|
+
- @unotest/dsl@0.31.0
|
|
267
|
+
- @unotest/grounder-client@0.31.0
|
|
268
|
+
|
|
269
|
+
## [0.30.0] - 2026-09-03
|
|
270
|
+
|
|
271
|
+
### Minor Changes
|
|
272
|
+
|
|
273
|
+
- 23de03c: On a hosted viewer (a box), the project and environment you are looking at
|
|
274
|
+
are now named and switched in the viewer's own tree header, in place of
|
|
275
|
+
the checkout directory it used to show there (`current`, which told
|
|
276
|
+
nobody anything). Previously the only way to another environment was
|
|
277
|
+
logging out and using the picker page. The chevrons appear on hover, like
|
|
278
|
+
the refresh button beside them, and only when something fronts the viewer
|
|
279
|
+
and offers it a list; a plain local viewer keeps the folder name it
|
|
280
|
+
always showed. Because
|
|
281
|
+
each environment on a box is its own viewer process, picking one is a full
|
|
282
|
+
page load into that viewer, and the menu shows which test bundle each
|
|
283
|
+
environment is running. The status bar's `.env.<name>` overlay switcher is
|
|
284
|
+
now labelled "env overlay", so the two senses of "environment" no longer
|
|
285
|
+
share a name. `@unotest/viewer/session` carries the two URLs this needs
|
|
286
|
+
(`environmentsUrl`, `switchEnvUrl`) and the `ViewerEnvOption` type, both
|
|
287
|
+
optional — a proxy that does not offer them gets the old behaviour.
|
|
288
|
+
|
|
289
|
+
### Patch Changes
|
|
290
|
+
|
|
291
|
+
- A run started in the first moments after the viewer boots is now discovered: the run watcher finishes its initial scan of the day directory before reporting itself started, instead of filing such a run as history.
|
|
292
|
+
- 3f5cab7: `bundle push` and `env push` no longer send you to "the box's
|
|
293
|
+
admin page" for a project token — there is no such page; tokens are
|
|
294
|
+
issued per project by the box's operator, and the refusal hints and the
|
|
295
|
+
missing-token message now say so.
|
|
296
|
+
- 3f5cab7: The vocabulary's description of `evaluate` now says how extra
|
|
297
|
+
arguments reach the body — none → nothing, exactly one → the value
|
|
298
|
+
itself, two or more → one array — matching the runtime, the linter's
|
|
299
|
+
hint and the authoring skill; it used to read as if they were positional
|
|
300
|
+
(`evaluate(js, ...args)`), and so did the generated DSL typings.
|
|
301
|
+
- 3f5cab7: A scenario that answers a native `confirm()` — a "Remove" button that
|
|
302
|
+
asks first, say — no longer fails when `run_test` auto-attaches the MCP
|
|
303
|
+
session to the run's browser, or after `attach_debug_session`. The
|
|
304
|
+
attaching client now applies the same dialog policy the run does
|
|
305
|
+
(`dialogPolicy`, accept by default) to the page it watches; before, it
|
|
306
|
+
listened for no dialogs at all, and Playwright dismisses every dialog on
|
|
307
|
+
behalf of a client that has no listener — so the second pair of eyes was
|
|
308
|
+
answering "no" before the run's own accept could land. The same scenario
|
|
309
|
+
passed with an exploration session open only because that session
|
|
310
|
+
happened to keep the run's browser to itself. Under `manual` the attached
|
|
311
|
+
client leaves the dialog to whoever handles it instead of dismissing it.
|
|
312
|
+
- 9f2e244: On a box, an administrator can now see a secret's value from the guard's
|
|
313
|
+
values page — the eye reveals it in the row, the copy button puts it on
|
|
314
|
+
the clipboard without showing it — and every look is a `secret.revealed`
|
|
315
|
+
entry in the box's audit trail, naming who, which secret and whether it
|
|
316
|
+
was shown or copied. The page is one table now (target, variables,
|
|
317
|
+
secrets), edited in place, with a JSON view that changes the whole
|
|
318
|
+
environment at once; a secret left as `"••••"` there is kept as it is.
|
|
319
|
+
The token door `env push` / `env set` talk to is unchanged: secrets stay
|
|
320
|
+
write-only on the wire.
|
|
321
|
+
|
|
322
|
+
`@unotest/protocol` carries the contract: `boxEnvSecretRevealPath` /
|
|
323
|
+
`boxEnvSecretsRevealPath` with their response parsers, the session-door
|
|
324
|
+
batch `BoxEnvValuesReplaceRequest` (`null` for a secret means "keep") and
|
|
325
|
+
`parseBoxEnvValuesReplaceRequest`, and the refusal code `unknown-secret`
|
|
326
|
+
for a reveal or a keep that names a secret the environment does not
|
|
327
|
+
hold. `isEnvVarName` now refuses `__proto__`: it is spelled like a
|
|
328
|
+
variable, but a plain object cannot hold it and the value was silently
|
|
329
|
+
lost. The CLI explains an `unknown-secret` refusal.
|
|
330
|
+
|
|
331
|
+
- 3f5cab7: `evaluate(js, …args)` has always handed the body its extra arguments in
|
|
332
|
+
one of three shapes — none, the single value, or ONE array for two or
|
|
333
|
+
more — while the authoring skill described them as positional. The
|
|
334
|
+
skill now states the rule with both spellings
|
|
335
|
+
(`evaluate('function(n){…}', 21)` / `evaluate('([a, b]) => …', a, b)`),
|
|
336
|
+
and the `lint:evaluate-discouraged` warning adds the same hint when it
|
|
337
|
+
sees two or more extra arguments; the runtime is unchanged. The skill
|
|
338
|
+
also gains the getters it left out (`count`, `getInputValue`,
|
|
339
|
+
`textContains`), the note that `waitForText` never sees a textarea's
|
|
340
|
+
value, the parser's refusal of `!` / `indexOf` / string methods, the
|
|
341
|
+
shared-stand rule against "the first row" (mark your own data, filter
|
|
342
|
+
by it, clean up), that native dialogs are accepted by the runner, and
|
|
343
|
+
what to do when the grounder is unavailable.
|
|
344
|
+
- 3f5cab7: While recording with `explore_step` / `explore_steps`, the bare NAME of a
|
|
345
|
+
scenario variable is substituted only where the step types a value into
|
|
346
|
+
the page — `fill`'s value, `press`'s key, `select_option`'s value,
|
|
347
|
+
`goto`'s url. In a locator's text or an assertion's expected text a bare
|
|
348
|
+
name is now the literal it looks like: `assertText(el, "BOX_LAB_PASSWORD")`
|
|
349
|
+
expects those letters, where before the live step silently waited for
|
|
350
|
+
the secret's value and the saved test said something else. To reference
|
|
351
|
+
a variable in a matcher, write `{{NAME}}`; the reply warns when a literal
|
|
352
|
+
happens to equal a variable's name. And a secret's value typed into a
|
|
353
|
+
value field is recorded as the secret's name (the reply says which), so
|
|
354
|
+
the value never lands in the test file.
|
|
355
|
+
- 3f5cab7: `run_test` no longer reports `spawn_failed` about a runner that is still
|
|
356
|
+
starting. Two runs launched together on a busy box took longer than the
|
|
357
|
+
fixed 10-second wait to write their first `runtime.json`, and the second
|
|
358
|
+
was declared dead while it was booting a browser. The wait now lasts as
|
|
359
|
+
long as the runner child is alive (up to 60 seconds), ends early when the
|
|
360
|
+
child joins the run queue, and — when the child really dies — says so with
|
|
361
|
+
its exit code and the last lines of its stderr, which the runner now
|
|
362
|
+
writes to `spawn.stderr.log` in its run directory.
|
|
363
|
+
|
|
364
|
+
`explore_start` now reports whether the grounder is usable for the
|
|
365
|
+
session: `grounder: {available: true}`, or `{available: false, reason,
|
|
366
|
+
hint}` when grounding is off, the backend is unreachable, or a required
|
|
367
|
+
model is not pulled (`"embeddinggemma" is not available there — pull it:
|
|
368
|
+
\`ollama pull embeddinggemma\``). An intent step in such a session fails
|
|
369
|
+
with the same words, not with the backend's raw error; the probe is one
|
|
370
|
+
short request per session.
|
|
371
|
+
|
|
372
|
+
Secret masking no longer rewrites the runner's own identifiers in
|
|
373
|
+
`runtime.json`: with `LOGIN=admin`, the scenario path `guard-admin.js`,
|
|
374
|
+
the test function `test_guard_admin` and the run id kept their names in
|
|
375
|
+
`inspect_runtime` replies instead of turning into `guard-‹secret:LOGIN›.js`.
|
|
376
|
+
Masking by value is unchanged everywhere else; the run id, scenario
|
|
377
|
+
path, test function and call-stack names are an explicit exception.
|
|
378
|
+
|
|
379
|
+
- 3f5cab7: `save_exploration_as_test` (and `generate_dsl_from_exploration`) now
|
|
380
|
+
write scoped locators — a `getByRole` / `getByText` / `locator(css)` step
|
|
381
|
+
under a parent, such as the "Reveal" button inside one table row — as
|
|
382
|
+
the chain the DSL already runs:
|
|
383
|
+
`locator('tr[data-row="A"]').getByRole('button', {name: 'Reveal', exact: true})`.
|
|
384
|
+
Before, any recorded step whose locator went below its root was skipped
|
|
385
|
+
with `NO_DSL_PRIMITIVE`, and the save was refused, although the step
|
|
386
|
+
had executed and the hand-written form worked. Every locator is now
|
|
387
|
+
rendered in the same method-chain spelling — narrowing steps too
|
|
388
|
+
(`getByRole('row').filter({hasText: 'Alice'}).first()` instead of
|
|
389
|
+
`first(filter(getByRole('row'), {hasText: 'Alice'}))`); both spellings
|
|
390
|
+
remain valid input, the runtime is unchanged.
|
|
391
|
+
- Updated dependencies [23de03c]
|
|
392
|
+
- Updated dependencies [9f2e244]
|
|
393
|
+
- @unotest/viewer@0.30.0
|
|
394
|
+
- @unotest/protocol@0.30.0
|
|
395
|
+
- @unotest/core@0.30.0
|
|
396
|
+
- @unotest/dsl@0.30.0
|
|
397
|
+
- @unotest/grounder-client@0.30.0
|
|
398
|
+
|
|
3
399
|
## [0.29.0] - 2026-09-02
|
|
4
400
|
|
|
5
401
|
### Minor Changes
|
package/bin/unotest-web.js
CHANGED
|
@@ -75,7 +75,9 @@ function dispatch(sub) {
|
|
|
75
75
|
forwardArgs: args.slice(1),
|
|
76
76
|
};
|
|
77
77
|
case "lint":
|
|
78
|
-
|
|
78
|
+
// Forwarded whole, like e2e: `--env <name>` and explicit file
|
|
79
|
+
// arguments belong to the command, not to the dispatcher.
|
|
80
|
+
return { dist: "dist/runner/cli.js", src: "src/runner/cli.ts", forwardArgs: args };
|
|
79
81
|
case "viewer":
|
|
80
82
|
return { dist: "dist/runner/cli.js", src: "src/runner/cli.ts", forwardArgs: ["viewer"] };
|
|
81
83
|
case "prepare-fix":
|