@unotest/web 0.49.1 → 0.49.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -3
- package/README.md +5 -4
- package/bin/unotest-web.js +10 -9
- package/dist/config/schema.d.ts +11 -11
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +2 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +4 -2
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +1 -1
- package/dist/inspection/page-inject.js +15 -9
- package/dist/{interfaces-D5ePwBX6.d.ts → interfaces-mlRtu8mL.d.ts} +10 -0
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init/chrome-probe.js +1 -1
- package/dist/runner/init.js +1 -1
- package/dist/runner/install-chromium.js +1 -1
- package/dist/runner/prepare-fix.js +1 -1
- package/dist/runner/scaffold-workspace.js +1 -1
- package/dist/runner/serve-fixture.js +1 -1
- package/dist/runner/web-runner-adapter.js +1 -1
- package/guides/agent-integration.md +15 -1
- package/guides/dsl-reference.md +94 -14
- package/package.json +8 -8
- package/types/unotest-dsl.d.ts +29 -15
package/guides/dsl-reference.md
CHANGED
|
@@ -113,12 +113,13 @@ function test_control_questions() {
|
|
|
113
113
|
|
|
114
114
|
Scenarios are plain `.js` with an implicit global vocabulary, so an
|
|
115
115
|
untouched editor shows them as broken JavaScript. `npx @unotest/web init`
|
|
116
|
-
wires the fix; on an existing project
|
|
117
|
-
|
|
118
|
-
consists of:
|
|
116
|
+
wires the fix; on an existing project, or a fresh checkout,
|
|
117
|
+
`npx @unotest/web types` does. A run never writes the editor config. What
|
|
118
|
+
it consists of:
|
|
119
119
|
|
|
120
120
|
- `unotest/jsconfig.json` — points the IDE at the two declaration files
|
|
121
|
-
below.
|
|
121
|
+
below. Written by `init` (which also adds it to `.gitignore`) and by
|
|
122
|
+
`types` when missing; never rewritten silently. Type
|
|
122
123
|
CHECKING stays off (`checkJs: false`) on purpose: diagnostics come from
|
|
123
124
|
the DSL validator (`npx @unotest/web lint`), the editor only gains
|
|
124
125
|
completion, hover docs, signature help and locator-chain resolution.
|
|
@@ -211,14 +212,19 @@ instead of waiting the default.
|
|
|
211
212
|
|
|
212
213
|
## Builders
|
|
213
214
|
|
|
214
|
-
### `locator(selector)`
|
|
215
|
+
### `locator(selector, options?)`
|
|
215
216
|
|
|
216
217
|
CSS selector, last-resort escape hatch. Use only when nothing else works
|
|
217
218
|
— the linter discourages this with `lint:deep-css` for `>`-bearing
|
|
218
219
|
selectors.
|
|
219
220
|
|
|
221
|
+
`options` narrows the matches exactly as [`filter`](#filteroptions)
|
|
222
|
+
does: the same keys (`hasText`, `hasNotText`, `has`, `hasNot`), the same
|
|
223
|
+
refusal of any other key, at lint time and at run time.
|
|
224
|
+
|
|
220
225
|
```js
|
|
221
226
|
locator('.error-banner')
|
|
227
|
+
locator('option', {hasText: 'Beta'}) // = locator('option').filter({hasText: 'Beta'})
|
|
222
228
|
```
|
|
223
229
|
|
|
224
230
|
### `getByRole(role, options?)`
|
|
@@ -227,10 +233,20 @@ ARIA role. `options.name` is the accessible name — string or regex
|
|
|
227
233
|
(see [Matcher arguments](#matcher-arguments--string--regexp)).
|
|
228
234
|
Substring by default, exact with `{exact: true}`.
|
|
229
235
|
|
|
236
|
+
The other options narrow by ARIA state, as Playwright's `getByRole` does:
|
|
237
|
+
`checked`, `disabled`, `expanded`, `pressed`, `selected` (true / false),
|
|
238
|
+
`level` (a heading level from 1) and `includeHidden` (true — match
|
|
239
|
+
elements hidden from the accessibility tree too). Any other key is
|
|
240
|
+
refused, at lint time (`lint:role-unknown-option`) and at run time — it
|
|
241
|
+
would otherwise match without it.
|
|
242
|
+
|
|
230
243
|
```js
|
|
231
244
|
getByRole('button', {name: 'Save'})
|
|
232
245
|
getByRole('heading', {name: 'Welcome', exact: true})
|
|
233
246
|
getByRole('link', {name: /^Sets/}) // regex matcher
|
|
247
|
+
getByRole('button', {name: 'Save', disabled: false}) // the enabled one
|
|
248
|
+
getByRole('checkbox', {checked: true})
|
|
249
|
+
getByRole('heading', {level: 2})
|
|
234
250
|
```
|
|
235
251
|
|
|
236
252
|
### `getByText(text, options?)`
|
|
@@ -375,10 +391,19 @@ press(getByLabel('Body'), 'Control+A')
|
|
|
375
391
|
|
|
376
392
|
### `check(loc, options?)` / `uncheck(loc, options?)`
|
|
377
393
|
|
|
378
|
-
Idempotent — no
|
|
394
|
+
Idempotent — no click if already in the target state. Otherwise one
|
|
395
|
+
click, then a wait until the element reads checked (unchecked) within
|
|
396
|
+
the action timeout (`options.timeout`, else the config default). A
|
|
397
|
+
controlled switch the app flips a beat after the click — once the server
|
|
398
|
+
confirms — passes; one that never flips fails, naming the state it
|
|
399
|
+
wanted and the last one it saw. Works on native checkboxes and radios
|
|
400
|
+
and on ARIA `checkbox` / `radio` / `switch` widgets. A `<label>` — or
|
|
401
|
+
anything inside one — stands for the control it names (`for=` or a
|
|
402
|
+
wrapped input). Any other element is refused — use `click` for it.
|
|
379
403
|
|
|
380
404
|
```js
|
|
381
405
|
check(getByRole('checkbox', {name: 'Subscribe'}))
|
|
406
|
+
uncheck(getByRole('switch', {name: 'Notifications'}), {timeout: 5000})
|
|
382
407
|
```
|
|
383
408
|
|
|
384
409
|
### `hover(loc, options?)`
|
|
@@ -844,10 +869,36 @@ assertCount(getByRole('listitem').filter({hasNotText: 'Archived'}), 3)
|
|
|
844
869
|
|
|
845
870
|
Empty string when the element has no text node (does not throw).
|
|
846
871
|
|
|
872
|
+
### `texts(loc)` → `string[]`
|
|
873
|
+
|
|
874
|
+
`textContent` of every element the locator matches, in page order; `[]`
|
|
875
|
+
when nothing matches. One read, no wait — the list to branch on.
|
|
876
|
+
`assertTexts` is the waiting check of the same list.
|
|
877
|
+
|
|
878
|
+
```js
|
|
879
|
+
names = texts(locator('#agents li')); // ["Claude Code", "Codex"]
|
|
880
|
+
if (names.length > 0) {
|
|
881
|
+
log('first agent:', names[0]);
|
|
882
|
+
}
|
|
883
|
+
```
|
|
884
|
+
|
|
847
885
|
### `inputValue(loc)` → `string`
|
|
848
886
|
|
|
849
887
|
### `isVisible(loc)` → `boolean`
|
|
850
888
|
|
|
889
|
+
### `isChecked(loc)` → `boolean`
|
|
890
|
+
|
|
891
|
+
Checked state of a checkbox, radio or switch — native or ARIA role
|
|
892
|
+
(`aria-checked`); a `<label>` reads as the control it names. Any other
|
|
893
|
+
element fails the step instead of answering
|
|
894
|
+
`false`: "unchecked" and "has no checked state" are different answers.
|
|
895
|
+
|
|
896
|
+
```js
|
|
897
|
+
if (isChecked(getByRole('switch', {name: 'Notifications'}))) {
|
|
898
|
+
uncheck(getByRole('switch', {name: 'Notifications'}));
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
851
902
|
### `isDisabled(loc)` → `boolean` / `isEnabled(loc)` → `boolean`
|
|
852
903
|
|
|
853
904
|
Disabled = native `:disabled` (covers `<fieldset disabled>` inheritance)
|
|
@@ -1746,12 +1797,21 @@ step("Send button uses the app's own label", () => {
|
|
|
1746
1797
|
generate the JSON from them instead — see
|
|
1747
1798
|
[App constants in scenarios](https://docs.unotest.com/concepts/scenarios/#app-constants-in-scenarios).
|
|
1748
1799
|
|
|
1749
|
-
### `env(name)` → `string | null`
|
|
1800
|
+
### `env(name, fallback?)` → `string | null`
|
|
1750
1801
|
|
|
1751
1802
|
An environment value the test can do without. It reads what a bare
|
|
1752
1803
|
`UPPER_SNAKE` name reads — the environment the run started in, then
|
|
1753
|
-
`unotest/.env` — but returns `
|
|
1754
|
-
of failing the step
|
|
1804
|
+
`unotest/.env` — but returns `fallback` for a name that is not set
|
|
1805
|
+
instead of failing the step, and `null` when no fallback is given.
|
|
1806
|
+
|
|
1807
|
+
The fallback is the one-line default — the DSL has no `??`, and `||`
|
|
1808
|
+
answers `true` / `false`, not a value:
|
|
1809
|
+
|
|
1810
|
+
```js
|
|
1811
|
+
api = env('API_URL', 'http://localhost:3000');
|
|
1812
|
+
```
|
|
1813
|
+
|
|
1814
|
+
Without a fallback, use the `null` to decide:
|
|
1755
1815
|
|
|
1756
1816
|
```js
|
|
1757
1817
|
if (env('LIVE_API') == null) {
|
|
@@ -1762,8 +1822,8 @@ if (env('LIVE_API') == null) {
|
|
|
1762
1822
|
```
|
|
1763
1823
|
|
|
1764
1824
|
A secret — a name from `unotest/.secrets`, or one a box injects — is
|
|
1765
|
-
refused: `env()` returns a plain string, and
|
|
1766
|
-
masked. Write the secret's bare name where its value is typed instead.
|
|
1825
|
+
refused, with a fallback or without: `env()` returns a plain string, and
|
|
1826
|
+
a credential must stay masked. Write the secret's bare name where its value is typed instead.
|
|
1767
1827
|
|
|
1768
1828
|
### `readFile(path, options?)` → `string`
|
|
1769
1829
|
|
|
@@ -2325,6 +2385,12 @@ result = evaluate(
|
|
|
2325
2385
|
Backticks are **raw** — `${…}` interpolation is rejected at parse time
|
|
2326
2386
|
(see `vendor/dsl/EXTENSIONS.md`).
|
|
2327
2387
|
|
|
2388
|
+
A page whose main thread is busy — a script loop, a long task — cannot
|
|
2389
|
+
start the script. `evaluate` then fails the step after the action timeout
|
|
2390
|
+
(the config's `defaultTimeoutMs`) with `the page gave no answer` instead
|
|
2391
|
+
of waiting forever. Once the script has started, it runs as long as it
|
|
2392
|
+
needs: an `async` body that awaits a slow promise is not cut off.
|
|
2393
|
+
|
|
2328
2394
|
`evaluateOnLocator(loc, js, ...args)` — the same hatch on one element.
|
|
2329
2395
|
The body gets the element first, then the extras by the same rule as
|
|
2330
2396
|
`evaluate`: none → `undefined`, one → the value itself, two or more → one
|
|
@@ -2499,7 +2565,12 @@ step.soft("Question", {tag: q.id}, () => {
|
|
|
2499
2565
|
- When a statement fails, the notes of its `step(...)` block (the
|
|
2500
2566
|
innermost one, in call order) come with the failure: under the error
|
|
2501
2567
|
in the `e2e` output as `note label: value`, and as `notes` in the
|
|
2502
|
-
failure `run_test` returns.
|
|
2568
|
+
failure `run_test` returns. Without `--notes` a green step prints
|
|
2569
|
+
nothing extra.
|
|
2570
|
+
- `e2e --notes` / `collection --notes` print every note of every test
|
|
2571
|
+
under its line, a passing test's too, as
|
|
2572
|
+
`note label: value — step "<step>"`. A failed test then lists them in
|
|
2573
|
+
place of the failing step's own, so each note prints once.
|
|
2503
2574
|
|
|
2504
2575
|
---
|
|
2505
2576
|
|
|
@@ -2568,6 +2639,14 @@ The DSL is JavaScript-shaped but parsed by a small AST engine
|
|
|
2568
2639
|
return **strings** — for comparing against a number use `==`
|
|
2569
2640
|
(`getAttribute(loc, "data-count") === 5` is always false; write
|
|
2570
2641
|
`== 5` or compare with a string).
|
|
2642
|
+
- **Ternary** — `cond ? a : b`, as in JS: the lowest precedence,
|
|
2643
|
+
nested to the right (`n == 0 ? 'none' : n == 1 ? 'one' : 'many'`),
|
|
2644
|
+
and allowed wherever a value goes (an argument, an object field, a
|
|
2645
|
+
`return`). Only the branch the condition picks runs, so
|
|
2646
|
+
`x == null ? '' : x.name` is a safe null guard. The condition is read
|
|
2647
|
+
as `if` reads it: `0`, `""`, `null` and `false` pick `b`. Where a slot
|
|
2648
|
+
expects a kind (a locator, a number), both branches must give it.
|
|
2649
|
+
`?.` and `??` are not in the DSL — use the ternary or `if` / `else`.
|
|
2571
2650
|
|
|
2572
2651
|
Not supported (and intentionally so): `while` / `do…while` /
|
|
2573
2652
|
`continue` (bound loops keep every scenario terminating), `++`/`--`,
|
|
@@ -2581,8 +2660,9 @@ inspection.
|
|
|
2581
2660
|
|
|
2582
2661
|
The only arrows are the fixed ones of `step(...)`, `teardown(...)` and
|
|
2583
2662
|
`waitUntil(...)`, and their body may be a block: `waitUntil(() => {
|
|
2584
|
-
n = count(rows); return n > 3; })`. A
|
|
2585
|
-
|
|
2663
|
+
n = count(rows); return n > 3; })`. A lambda goes only into `filter`,
|
|
2664
|
+
`map`, `find` and `some` on an array (`a.map(x => x.id)`); anywhere
|
|
2665
|
+
else, and `+=`, `for…of`, `o.a = 2` and the like, are refused by `lint`
|
|
2586
2666
|
with what to write instead.
|
|
2587
2667
|
|
|
2588
2668
|
See `vendor/dsl/EXTENSIONS.md` for the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/web",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.2",
|
|
4
4
|
"description": "AI-native E2E testing for web applications. MCP server (run_test / step / resume / inspect_runtime / agent_fix) + CLI runner + JavaScript DSL scenarios on a sandboxed AST engine + semantic DOM snapshots.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -85,18 +85,18 @@
|
|
|
85
85
|
"tsx": "^4.19.2",
|
|
86
86
|
"yaml": "^2.6.0",
|
|
87
87
|
"zod": "^3.23.8",
|
|
88
|
-
"@unotest/core": "^0.49.
|
|
89
|
-
"@unotest/
|
|
90
|
-
"@unotest/
|
|
91
|
-
"@unotest/
|
|
92
|
-
"@unotest/viewer": "^0.49.
|
|
88
|
+
"@unotest/core": "^0.49.2",
|
|
89
|
+
"@unotest/grounder-client": "^0.49.2",
|
|
90
|
+
"@unotest/dsl": "^0.49.2",
|
|
91
|
+
"@unotest/protocol": "^0.49.2",
|
|
92
|
+
"@unotest/viewer": "^0.49.2"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@types/node": "^22.10.0",
|
|
96
96
|
"javascript-obfuscator": "^4.1.1",
|
|
97
97
|
"tsup": "^8.5.1",
|
|
98
98
|
"typescript": "^5.7.2",
|
|
99
|
-
"@unotest/
|
|
100
|
-
"@unotest/
|
|
99
|
+
"@unotest/grounder": "^0.49.2",
|
|
100
|
+
"@unotest/judge": "^0.49.2"
|
|
101
101
|
}
|
|
102
102
|
}
|
package/types/unotest-dsl.d.ts
CHANGED
|
@@ -36,15 +36,15 @@ interface Locator {
|
|
|
36
36
|
*/
|
|
37
37
|
randomNth(): Locator;
|
|
38
38
|
/**
|
|
39
|
-
* locator(css)
|
|
39
|
+
* locator(css, { hasText?, hasNotText?, has?, hasNot? }?)
|
|
40
40
|
*
|
|
41
|
-
* Match by raw CSS selector. Last resort — the linter warns on deep/brittle CSS.
|
|
41
|
+
* Match by raw CSS selector. Last resort — the linter warns on deep/brittle CSS. Options narrow the matches exactly as filter() does.
|
|
42
42
|
*/
|
|
43
|
-
locator(selector: string): Locator;
|
|
43
|
+
locator(selector: string, options?: any): Locator;
|
|
44
44
|
/**
|
|
45
|
-
* getByRole(role, { name?, exact? })
|
|
45
|
+
* getByRole(role, { name?, exact?, checked?, disabled?, expanded?, includeHidden?, level?, pressed?, selected? })
|
|
46
46
|
*
|
|
47
|
-
* Match by ARIA role + accessible name. `name` accepts a string or `/regex
|
|
47
|
+
* Match by ARIA role + accessible name. `name` accepts a string or `/regex/`; the state options narrow by ARIA state (`{disabled: false}` skips a disabled button). Any other key is refused.
|
|
48
48
|
*/
|
|
49
49
|
getByRole(role: string, options?: any): Locator;
|
|
50
50
|
/**
|
|
@@ -108,14 +108,14 @@ declare function doubleClick(locator: Locator, options?: any): void;
|
|
|
108
108
|
/**
|
|
109
109
|
* check(locator, { force?, timeout? })
|
|
110
110
|
*
|
|
111
|
-
* Check a checkbox/radio.
|
|
111
|
+
* Check a checkbox/radio/switch: click, then wait until it reads checked (a controlled widget may flip a beat later). Already checked → no click.
|
|
112
112
|
*/
|
|
113
113
|
declare function check(locator: Locator, options?: any): void;
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* uncheck(locator, { force?, timeout? })
|
|
117
117
|
*
|
|
118
|
-
* Uncheck a checkbox.
|
|
118
|
+
* Uncheck a checkbox/switch: click, then wait until it reads unchecked. Already unchecked → no click.
|
|
119
119
|
*/
|
|
120
120
|
declare function uncheck(locator: Locator, options?: any): void;
|
|
121
121
|
|
|
@@ -504,6 +504,13 @@ declare function count(locator: Locator): number;
|
|
|
504
504
|
*/
|
|
505
505
|
declare function textContent(locator: Locator): string;
|
|
506
506
|
|
|
507
|
+
/**
|
|
508
|
+
* texts(locator) → string[]
|
|
509
|
+
*
|
|
510
|
+
* textContent of every match, in DOM order; [] when nothing matches. No wait — the list to branch on; assertTexts is the waiting check.
|
|
511
|
+
*/
|
|
512
|
+
declare function texts(locator: Locator): any;
|
|
513
|
+
|
|
507
514
|
/**
|
|
508
515
|
* inputValue(locator) → string
|
|
509
516
|
*
|
|
@@ -518,6 +525,13 @@ declare function inputValue(locator: Locator): string;
|
|
|
518
525
|
*/
|
|
519
526
|
declare function isVisible(locator: Locator): boolean;
|
|
520
527
|
|
|
528
|
+
/**
|
|
529
|
+
* isChecked(locator) → boolean
|
|
530
|
+
*
|
|
531
|
+
* Checked state of a checkbox/radio/switch (native or ARIA role). Fails on any other element instead of answering false.
|
|
532
|
+
*/
|
|
533
|
+
declare function isChecked(locator: Locator): boolean;
|
|
534
|
+
|
|
521
535
|
/**
|
|
522
536
|
* isDisabled(locator) → boolean
|
|
523
537
|
*
|
|
@@ -680,11 +694,11 @@ declare function round(n: number, digits?: number): number;
|
|
|
680
694
|
declare function formatNumber(n: number, options?: any): string;
|
|
681
695
|
|
|
682
696
|
/**
|
|
683
|
-
* env(name) → string | null
|
|
697
|
+
* env(name, fallback?) → string | fallback | null
|
|
684
698
|
*
|
|
685
|
-
* An environment value the test can do without: the ambient environment, unotest/.env or .secrets, as a bare UPPER_SNAKE name reads it — but null when unset instead of failing. For deciding on it: if (env('LIVE_API') == null) { … }. A secret is refused; write its bare name where the value is typed.
|
|
699
|
+
* An environment value the test can do without: the ambient environment, unotest/.env or .secrets, as a bare UPPER_SNAKE name reads it — but the fallback (null without one) when unset instead of failing. A default in one line: env('API_URL', 'http://localhost:3000'). For deciding on it: if (env('LIVE_API') == null) { … }. A secret is refused, fallback or not; write its bare name where the value is typed.
|
|
686
700
|
*/
|
|
687
|
-
declare function env(name: string): any;
|
|
701
|
+
declare function env(name: string, fallback?: any): any;
|
|
688
702
|
|
|
689
703
|
/**
|
|
690
704
|
* shell(cmd, ...args, options?) → { stdout, stderr, code }
|
|
@@ -792,16 +806,16 @@ declare function apiCall(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "
|
|
|
792
806
|
declare function upload(path: string, options?: any): any;
|
|
793
807
|
|
|
794
808
|
/**
|
|
795
|
-
* locator(css)
|
|
809
|
+
* locator(css, { hasText?, hasNotText?, has?, hasNot? }?)
|
|
796
810
|
*
|
|
797
|
-
* Match by raw CSS selector. Last resort — the linter warns on deep/brittle CSS.
|
|
811
|
+
* Match by raw CSS selector. Last resort — the linter warns on deep/brittle CSS. Options narrow the matches exactly as filter() does.
|
|
798
812
|
*/
|
|
799
|
-
declare function locator(selector: string): Locator;
|
|
813
|
+
declare function locator(selector: string, options?: any): Locator;
|
|
800
814
|
|
|
801
815
|
/**
|
|
802
|
-
* getByRole(role, { name?, exact? })
|
|
816
|
+
* getByRole(role, { name?, exact?, checked?, disabled?, expanded?, includeHidden?, level?, pressed?, selected? })
|
|
803
817
|
*
|
|
804
|
-
* Match by ARIA role + accessible name. `name` accepts a string or `/regex
|
|
818
|
+
* Match by ARIA role + accessible name. `name` accepts a string or `/regex/`; the state options narrow by ARIA state (`{disabled: false}` skips a disabled button). Any other key is refused.
|
|
805
819
|
*/
|
|
806
820
|
declare function getByRole(role: string, options?: any): Locator;
|
|
807
821
|
|