@unotest/web 0.49.0 → 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 +144 -3
- package/README.md +5 -4
- package/bin/unotest-web.js +34 -17
- package/dist/config/schema.d.ts +11 -11
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +8 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +10 -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 +17 -2
- package/dist/inspection/page-inject.js +36 -9
- package/dist/{interfaces-CTMGjJIs.d.ts → interfaces-mlRtu8mL.d.ts} +23 -1
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.d.ts +3 -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 +15 -1
- package/guides/dsl-reference.md +256 -28
- package/package.json +8 -8
- package/types/unotest-dsl.d.ts +37 -16
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?)`
|
|
@@ -446,8 +471,16 @@ list that is still filling in passes once it is complete.
|
|
|
446
471
|
|
|
447
472
|
```js
|
|
448
473
|
assertTexts(getByRole('listitem'), ['Apple', 'Banana', /^Cherry/])
|
|
474
|
+
labels = ['Apple', 'Banana'];
|
|
475
|
+
assertTexts(getByRole('listitem'), labels)
|
|
476
|
+
assertText(getByRole('listitem').first(), labels[0])
|
|
449
477
|
```
|
|
450
478
|
|
|
479
|
+
`expected` is any expression that holds a list: a literal, a variable,
|
|
480
|
+
an index read, a helper's result. A value known to be something else (a
|
|
481
|
+
string, a number) is refused by the linter; the rest is checked when the
|
|
482
|
+
step runs. The same holds for the expected text of `assertText`.
|
|
483
|
+
|
|
451
484
|
**Options:** `timeout?`, `exact?` (default `true`).
|
|
452
485
|
|
|
453
486
|
### `assertAttribute(loc, name, expected, options?)`
|
|
@@ -610,18 +643,22 @@ function test_live_payment() {
|
|
|
610
643
|
}
|
|
611
644
|
```
|
|
612
645
|
|
|
613
|
-
A decision on data the test reads goes after the step that reads it
|
|
646
|
+
A decision on data the test reads goes after the step that reads it, or
|
|
647
|
+
inside that step:
|
|
614
648
|
|
|
615
649
|
```js
|
|
616
650
|
function test_refund() {
|
|
617
651
|
step("Find a paid order", () => {
|
|
618
652
|
orders = apiCall('GET', '/orders?status=paid');
|
|
653
|
+
if (orders.length == 0) { skip('no paid order on this stand'); }
|
|
619
654
|
});
|
|
620
|
-
skip('no paid order on this stand', orders.length == 0);
|
|
621
655
|
// …
|
|
622
656
|
}
|
|
623
657
|
```
|
|
624
658
|
|
|
659
|
+
The rest of the test does not run; teardown blocks already registered
|
|
660
|
+
still play.
|
|
661
|
+
|
|
625
662
|
- The CLI prints `○ test: … skipped: <reason>` and counts it apart:
|
|
626
663
|
`0 passed, 0 failed, 0 errored, 1 skipped`. The exit code is 0.
|
|
627
664
|
- A collection counts it as `skipped=N(test=N)`.
|
|
@@ -630,9 +667,8 @@ function test_refund() {
|
|
|
630
667
|
skipped test whose teardown fails.
|
|
631
668
|
- The condition is `true` or `false`; anything else fails the test with
|
|
632
669
|
the value named — compare it: `env('LIVE_API') == null`.
|
|
633
|
-
- Call it
|
|
634
|
-
|
|
635
|
-
`lint:skip-outside-test`.
|
|
670
|
+
- Call it in the `test_*` body — at its top or inside a step or a block.
|
|
671
|
+
In a helper or a `flow_*` it is reported by `lint:skip-outside-test`.
|
|
636
672
|
|
|
637
673
|
### `assertJudge(loc, rubric)`
|
|
638
674
|
|
|
@@ -816,14 +852,53 @@ Read state without asserting. No polling.
|
|
|
816
852
|
|
|
817
853
|
### `count(loc)` → `number`
|
|
818
854
|
|
|
855
|
+
How many elements match now. To count by a condition, narrow the
|
|
856
|
+
locator: by text or by a child — `.filter({...})`; by the element's own
|
|
857
|
+
class or attribute — CSS. `assertCount` / `waitForCount` take the same
|
|
858
|
+
locator and wait for the number.
|
|
859
|
+
|
|
860
|
+
```js
|
|
861
|
+
done = count(getByRole('row').filter({hasText: 'Done'}));
|
|
862
|
+
admins = count(getByRole('row').filter({has: getByTestId('admin-badge')}));
|
|
863
|
+
selected = count(locator('li.selected'));
|
|
864
|
+
external = count(locator('a[target="_blank"]'));
|
|
865
|
+
assertCount(getByRole('listitem').filter({hasNotText: 'Archived'}), 3)
|
|
866
|
+
```
|
|
867
|
+
|
|
819
868
|
### `textContent(loc)` → `string`
|
|
820
869
|
|
|
821
870
|
Empty string when the element has no text node (does not throw).
|
|
822
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
|
+
|
|
823
885
|
### `inputValue(loc)` → `string`
|
|
824
886
|
|
|
825
887
|
### `isVisible(loc)` → `boolean`
|
|
826
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
|
+
|
|
827
902
|
### `isDisabled(loc)` → `boolean` / `isEnabled(loc)` → `boolean`
|
|
828
903
|
|
|
829
904
|
Disabled = native `:disabled` (covers `<fieldset disabled>` inheritance)
|
|
@@ -1719,15 +1794,24 @@ step("Send button uses the app's own label", () => {
|
|
|
1719
1794
|
- No poll: a missing file fails at once with `file not found: <path>`,
|
|
1720
1795
|
a file that does not parse with `<path> is not valid JSON: …`.
|
|
1721
1796
|
- Scenarios cannot import your app's modules, and that is on purpose:
|
|
1722
|
-
generate the JSON from them instead
|
|
1723
|
-
scenarios
|
|
1797
|
+
generate the JSON from them instead — see
|
|
1798
|
+
[App constants in scenarios](https://docs.unotest.com/concepts/scenarios/#app-constants-in-scenarios).
|
|
1724
1799
|
|
|
1725
|
-
### `env(name)` → `string | null`
|
|
1800
|
+
### `env(name, fallback?)` → `string | null`
|
|
1726
1801
|
|
|
1727
1802
|
An environment value the test can do without. It reads what a bare
|
|
1728
1803
|
`UPPER_SNAKE` name reads — the environment the run started in, then
|
|
1729
|
-
`unotest/.env` — but returns `
|
|
1730
|
-
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:
|
|
1731
1815
|
|
|
1732
1816
|
```js
|
|
1733
1817
|
if (env('LIVE_API') == null) {
|
|
@@ -1738,8 +1822,8 @@ if (env('LIVE_API') == null) {
|
|
|
1738
1822
|
```
|
|
1739
1823
|
|
|
1740
1824
|
A secret — a name from `unotest/.secrets`, or one a box injects — is
|
|
1741
|
-
refused: `env()` returns a plain string, and
|
|
1742
|
-
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.
|
|
1743
1827
|
|
|
1744
1828
|
### `readFile(path, options?)` → `string`
|
|
1745
1829
|
|
|
@@ -2057,6 +2141,29 @@ assertTrue(computedStyle(getByRole('dialog'), 'opacity') == 1, 'dialog is faded'
|
|
|
2057
2141
|
accent = computedStyle(locator('body'), '--accent');
|
|
2058
2142
|
```
|
|
2059
2143
|
|
|
2144
|
+
### `getImageSize(loc)` → `{width, height}` | `null`
|
|
2145
|
+
|
|
2146
|
+
The natural size of an `<img>` — the pixels of the file, not its size on
|
|
2147
|
+
the page (that is `boundingBox`). `null` when the image has no pixels:
|
|
2148
|
+
the file is broken, or there is no `src`. The one query that waits: an
|
|
2149
|
+
image still loading is waited for up to the action timeout, so a good
|
|
2150
|
+
image is never read too early as broken.
|
|
2151
|
+
|
|
2152
|
+
- Not an `<img>` fails with a `LocatorError` naming the tag. An inline
|
|
2153
|
+
`<svg>` has no natural size — `boundingBox` gives its rendered one.
|
|
2154
|
+
- Still loading when the timeout runs out fails with a `TimeoutError`.
|
|
2155
|
+
A `loading="lazy"` image below the fold never starts —
|
|
2156
|
+
`scrollIntoView(loc)` it first.
|
|
2157
|
+
- An SVG file without its own `width` / `height` reports the browser's
|
|
2158
|
+
default (300 × 150 in Chromium): check it is not `null`, not the
|
|
2159
|
+
numbers.
|
|
2160
|
+
|
|
2161
|
+
```js
|
|
2162
|
+
size = getImageSize(getByRole('img', {name: 'Avatar'}));
|
|
2163
|
+
assertTrue(size !== null, 'avatar is broken');
|
|
2164
|
+
assertTrue(size.width >= 256, json(size));
|
|
2165
|
+
```
|
|
2166
|
+
|
|
2060
2167
|
### `hasAnimation(loc)` → `boolean`
|
|
2061
2168
|
|
|
2062
2169
|
Whether the element or a descendant has an animation still running
|
|
@@ -2278,6 +2385,12 @@ result = evaluate(
|
|
|
2278
2385
|
Backticks are **raw** — `${…}` interpolation is rejected at parse time
|
|
2279
2386
|
(see `vendor/dsl/EXTENSIONS.md`).
|
|
2280
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
|
+
|
|
2281
2394
|
`evaluateOnLocator(loc, js, ...args)` — the same hatch on one element.
|
|
2282
2395
|
The body gets the element first, then the extras by the same rule as
|
|
2283
2396
|
`evaluate`: none → `undefined`, one → the value itself, two or more → one
|
|
@@ -2452,7 +2565,12 @@ step.soft("Question", {tag: q.id}, () => {
|
|
|
2452
2565
|
- When a statement fails, the notes of its `step(...)` block (the
|
|
2453
2566
|
innermost one, in call order) come with the failure: under the error
|
|
2454
2567
|
in the `e2e` output as `note label: value`, and as `notes` in the
|
|
2455
|
-
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.
|
|
2456
2574
|
|
|
2457
2575
|
---
|
|
2458
2576
|
|
|
@@ -2466,7 +2584,17 @@ The DSL is JavaScript-shaped but parsed by a small AST engine
|
|
|
2466
2584
|
viewer shows it as its own row under the file. `e2e <file>` plays them
|
|
2467
2585
|
in order; `e2e <file> --test <name>` plays one. Functions of the file
|
|
2468
2586
|
are shared between its tests, state is not.
|
|
2469
|
-
- **
|
|
2587
|
+
- **Top level of a file — function definitions and `const` literals.**
|
|
2588
|
+
Nothing else there runs: `BASE = 'x'` above `test_*` would leave
|
|
2589
|
+
`BASE` undefined in the test, so `lint` refuses it
|
|
2590
|
+
(`validator:unsupported-statement`). A shared fixed value is a const:
|
|
2591
|
+
`const BASE = 'https://x.test';` — a literal only (number, string,
|
|
2592
|
+
`true` / `false` / `null`, or an array or object of those), visible in
|
|
2593
|
+
every function, as helper functions are. A computed one is a function:
|
|
2594
|
+
`function base_url() { return env('BASE'); }`.
|
|
2595
|
+
- **Assignments** — `name = expression;`, or `let name = …;` /
|
|
2596
|
+
`const name = …;`: see [let, const and lambdas](#let-const-and-lambdas).
|
|
2597
|
+
`var` is refused.
|
|
2470
2598
|
- **`if` / `else`** and **`for` in the canonical shape**
|
|
2471
2599
|
`for (i = 0; i < N; i = i + 1) { … }` (increment must be an
|
|
2472
2600
|
assignment — `i++` is rejected everywhere). A loop over cases, each a
|
|
@@ -2490,8 +2618,8 @@ The DSL is JavaScript-shaped but parsed by a small AST engine
|
|
|
2490
2618
|
- **Index access on data** — `docs[0]`, `docs[0].objectsCount`,
|
|
2491
2619
|
`res.body.items[0].id`: see [Strings, objects, indexes](#strings-objects-indexes).
|
|
2492
2620
|
Indexing a **Locator** is rejected — disambiguate with chain methods
|
|
2493
|
-
(`.filter(...)`)
|
|
2494
|
-
`arr.
|
|
2621
|
+
(`.filter(...)`). `arr[i] = v` replaces an element, `arr.push(v)`
|
|
2622
|
+
appends, `arr.sortBy(key)` sorts: see [Logical not and arrays](#logical-not-and-arrays).
|
|
2495
2623
|
- **Object literals** — as call arguments (`click(loc, {force: true})`)
|
|
2496
2624
|
and as values in a variable: see [Strings, objects, indexes](#strings-objects-indexes).
|
|
2497
2625
|
- **Member access** — `row.email` (dot form; `obj['key']` doesn't
|
|
@@ -2504,20 +2632,39 @@ The DSL is JavaScript-shaped but parsed by a small AST engine
|
|
|
2504
2632
|
numeric strings coerce), `===` / `!==` (strict: type AND value must
|
|
2505
2633
|
match, `1 === "1"` is false), `<` `<=` `>` `>=`, logical `&&` / `||`
|
|
2506
2634
|
and `!` (see [Logical not and arrays](#logical-not-and-arrays)).
|
|
2635
|
+
`&&` and `||` short-circuit as in JS and give a boolean: the right side
|
|
2636
|
+
is skipped once the left one decides, so `x != null && x.a == 1` is a
|
|
2637
|
+
safe null guard (also inside `waitUntil`).
|
|
2507
2638
|
Caveat: DOM reads (`textContent`, `getInputValue`, `getAttribute`)
|
|
2508
2639
|
return **strings** — for comparing against a number use `==`
|
|
2509
2640
|
(`getAttribute(loc, "data-count") === 5` is always false; write
|
|
2510
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`.
|
|
2511
2650
|
|
|
2512
2651
|
Not supported (and intentionally so): `while` / `do…while` /
|
|
2513
2652
|
`continue` (bound loops keep every scenario terminating), `++`/`--`,
|
|
2514
|
-
|
|
2653
|
+
functions as values (a lambda goes only into `filter` / `map` / `find` /
|
|
2654
|
+
`some`), async/await (everything is sequenced by the AST
|
|
2515
2655
|
executor), classes, modules, promises, `try/catch` — for cleanup that
|
|
2516
2656
|
must run whatever happens, see [`teardown`](#teardown--give-the-stand-back).
|
|
2517
2657
|
Errors thrown from a primitive abort the scenario; if the runtime is
|
|
2518
2658
|
paused (debugger / MCP step mode), the failure is captured for
|
|
2519
2659
|
inspection.
|
|
2520
2660
|
|
|
2661
|
+
The only arrows are the fixed ones of `step(...)`, `teardown(...)` and
|
|
2662
|
+
`waitUntil(...)`, and their body may be a block: `waitUntil(() => {
|
|
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`
|
|
2666
|
+
with what to write instead.
|
|
2667
|
+
|
|
2521
2668
|
See `vendor/dsl/EXTENSIONS.md` for the
|
|
2522
2669
|
exact grammar deltas from the upstream parser.
|
|
2523
2670
|
|
|
@@ -2549,7 +2696,7 @@ assertText(getByRole('cell', {name: /^Total/}), '1\u00A0250'); // NBSP between
|
|
|
2549
2696
|
|
|
2550
2697
|
Since 0.44.0.
|
|
2551
2698
|
|
|
2552
|
-
**Strings** answer
|
|
2699
|
+
**Strings** answer these members — the whole list:
|
|
2553
2700
|
|
|
2554
2701
|
| Member | Returns | |
|
|
2555
2702
|
|---|---|---|
|
|
@@ -2557,6 +2704,7 @@ Since 0.44.0.
|
|
|
2557
2704
|
| `s.trim()` | `string` | whitespace off both ends |
|
|
2558
2705
|
| `s.includes(search)` | `boolean` | substring probe, same as `textContains(s, search)` |
|
|
2559
2706
|
| `s.startsWith(prefix)` | `boolean` | |
|
|
2707
|
+
| `s.split(separator)` | array of strings | as in JS: parts keep their spaces, a missing separator gives one part |
|
|
2560
2708
|
|
|
2561
2709
|
They work wherever an expression does — in `assertTrue`, in `if`, on a
|
|
2562
2710
|
literal, on a call result:
|
|
@@ -2566,9 +2714,10 @@ title = getTitle();
|
|
|
2566
2714
|
assertTrue(title.trim().length > 0, 'empty title');
|
|
2567
2715
|
if (out.stdout.includes('ready')) { log('up'); }
|
|
2568
2716
|
assertTrue(getUrl().startsWith('https://'), getUrl());
|
|
2717
|
+
ids = 'a, b'.split(', '); // ['a', 'b']
|
|
2569
2718
|
```
|
|
2570
2719
|
|
|
2571
|
-
Any other member is a lint error that names it and lists
|
|
2720
|
+
Any other member is a lint error that names it and lists them
|
|
2572
2721
|
(`validator:unsupported-string-member`); `s.padStart(3)` does not
|
|
2573
2722
|
reach the browser. A member of the list used on a value that turns out
|
|
2574
2723
|
not to be a string at run time — a number, or `null` from a missing
|
|
@@ -2609,8 +2758,9 @@ assertTrue(!out.stdout.includes('error'), out.stdout);
|
|
|
2609
2758
|
```
|
|
2610
2759
|
|
|
2611
2760
|
**`arr.push(x, …)`** appends to the array in place and returns its new
|
|
2612
|
-
length. Other array methods (`pop`, `
|
|
2613
|
-
`length`
|
|
2761
|
+
length. Other JS array methods (`pop`, `join`, `reduce`, …) are not
|
|
2762
|
+
there; `length`, `sortBy` and the lambda methods `filter` / `map` /
|
|
2763
|
+
`find` / `some` ([let, const and lambdas](#let-const-and-lambdas)) are.
|
|
2614
2764
|
|
|
2615
2765
|
```js
|
|
2616
2766
|
rows = getByRole('row');
|
|
@@ -2629,6 +2779,69 @@ that is not an array — a string, a number, a `null` field — fails the
|
|
|
2629
2779
|
step with that type in the message; on a string the linter flags it
|
|
2630
2780
|
first.
|
|
2631
2781
|
|
|
2782
|
+
**`arr[i] = v`** replaces element `i`, nested too (`grid[r][c] = v`); on
|
|
2783
|
+
an object it sets the key (`o["k"] = v`). The index is an integer from
|
|
2784
|
+
`0` to `arr.length`: the length itself appends, anything further out
|
|
2785
|
+
fails the step instead of leaving holes. The keys `__proto__`,
|
|
2786
|
+
`constructor` and `prototype` cannot be assigned, and an index read on
|
|
2787
|
+
an object sees its own keys only. Property assignment (`o.k = v`) stays
|
|
2788
|
+
unsupported.
|
|
2789
|
+
|
|
2790
|
+
**`arr.sortBy(key?, {desc?})`** sorts the array in place and returns it.
|
|
2791
|
+
With a key every element is an object and its field is compared;
|
|
2792
|
+
without one (`sortBy()`, or `sortBy(null, {desc: true})`) the elements
|
|
2793
|
+
themselves. The compared values are all numbers or all strings —
|
|
2794
|
+
anything else fails the step. The sort is stable: equal keys keep their
|
|
2795
|
+
order.
|
|
2796
|
+
|
|
2797
|
+
```js
|
|
2798
|
+
controls = [{id: 'b', order: 2}, {id: 'a', order: 1}];
|
|
2799
|
+
controls.sortBy('order'); // a, b
|
|
2800
|
+
controls.sortBy('order', {desc: true});
|
|
2801
|
+
controls[0] = {id: 'z', order: 0};
|
|
2802
|
+
```
|
|
2803
|
+
|
|
2804
|
+
### let, const and lambdas
|
|
2805
|
+
|
|
2806
|
+
**`let x = …;`** and **`const x = …;`** declare a variable. They are the
|
|
2807
|
+
bare assignment `x = …;` with a name for intent: the scope is the
|
|
2808
|
+
function's, as for any variable here — a block does not make one.
|
|
2809
|
+
`for (let i = 0; i < n; i = i + 1)` declares the loop variable. A const
|
|
2810
|
+
needs a value and is not assigned again: `lint` refuses it
|
|
2811
|
+
(`validator:const-assign`) and a run fails at that line, as JavaScript
|
|
2812
|
+
does. The array or object it holds can still change (`ids.push(x)`). `var`
|
|
2813
|
+
is refused.
|
|
2814
|
+
|
|
2815
|
+
A **lambda** — `x => expr`, `(x, i) => expr`, or a block with `return` —
|
|
2816
|
+
goes into exactly four array methods, as their argument:
|
|
2817
|
+
|
|
2818
|
+
| Call | Returns |
|
|
2819
|
+
|---|---|
|
|
2820
|
+
| `a.filter(x => cond)` | a new array of the elements for which `cond` is truthy |
|
|
2821
|
+
| `a.map((x, i) => value)` | a new array of the values |
|
|
2822
|
+
| `a.find(x => cond)` | the first element for which `cond` is truthy, or `null` |
|
|
2823
|
+
| `a.some(x => cond)` | `true` if `cond` is truthy for any element |
|
|
2824
|
+
|
|
2825
|
+
```js
|
|
2826
|
+
const rows = fetchData('orders');
|
|
2827
|
+
let open = rows.filter(r => r.status == 'open');
|
|
2828
|
+
ids = open.map((r, i) => i + ': ' + r.id);
|
|
2829
|
+
first = rows.find(r => {
|
|
2830
|
+
if (r.total > 100) { return true; }
|
|
2831
|
+
return false;
|
|
2832
|
+
});
|
|
2833
|
+
assertTrue(rows.some(r => r.id == '42'), json(ids));
|
|
2834
|
+
```
|
|
2835
|
+
|
|
2836
|
+
The parameters live in a scope of their own, like a helper's: the body
|
|
2837
|
+
reads the variables around it, and what it assigns stays inside. The
|
|
2838
|
+
body runs element by element, so a call in it — `textContent(…)`,
|
|
2839
|
+
`count(…)` — is a step of its own. A lambda is not a value: it cannot be
|
|
2840
|
+
assigned, returned or passed anywhere else (`validator:lambda-position`),
|
|
2841
|
+
and on a value that is not an array the step fails. With a lambda,
|
|
2842
|
+
`filter` is the array method; with an options object it is still the
|
|
2843
|
+
locator step `getByRole('row').filter({hasText: 'x'})`.
|
|
2844
|
+
|
|
2632
2845
|
### `teardown` — give the stand back
|
|
2633
2846
|
|
|
2634
2847
|
Since 0.44.0.
|
|
@@ -2758,8 +2971,21 @@ helpers. A function, parameter or variable of your own named
|
|
|
2758
2971
|
file's leading comments, set apart from the first function by a blank
|
|
2759
2972
|
line, mark every test in it. A marker trailing code (`foo(); //
|
|
2760
2973
|
@expect-fail`) or inside a body is a comment, not a marker.
|
|
2974
|
+
|
|
2975
|
+
A marked test that fails is reported as failed on purpose:
|
|
2976
|
+
`✗ test: test_x (…ms) — failed on purpose (@expect-fail)` in `e2e`,
|
|
2977
|
+
`N of M failed on purpose` and `expected=N` in a collection, and the
|
|
2978
|
+
viewer shows it as expected rather than failing. A collection whose
|
|
2979
|
+
only failures are on purpose exits 0; `e2e` of such a test still exits
|
|
2980
|
+
1 — the test did fail. A marked test that passes is a failure: the bug
|
|
2981
|
+
it guarded is gone, so either remove the marker or fix the test.
|
|
2761
2982
|
- **Helpers stay in `unotest/e2e/_helpers/`** — these are project-owned,
|
|
2762
2983
|
not part of the core surface. Use sandbox primitives there.
|
|
2984
|
+
- **A helper for one scenario can live in its file.** A function defined
|
|
2985
|
+
at the top level of a scenario is a helper like one in `_helpers/`,
|
|
2986
|
+
seen only by that file: the run calls it and `lint` knows its name. A
|
|
2987
|
+
misspelt call is reported wherever it stands, inside an object
|
|
2988
|
+
argument too (`apiCall('POST', '/pages', {html: page_html()})`).
|
|
2763
2989
|
- **Two kinds of helper.** A `flow_*` helper is a composite journey —
|
|
2764
2990
|
sign-in, checkout — and is *executable on its own*: the runner accepts it
|
|
2765
2991
|
as an entry and the agent can replay it while recording. A plain
|
|
@@ -2781,4 +3007,6 @@ helpers. A function, parameter or variable of your own named
|
|
|
2781
3007
|
and yields its result (`email = flow_signup()`), a rename hint when it
|
|
2782
3008
|
only reads.
|
|
2783
3009
|
- **Lint before you ship** — `npx @unotest/web lint` runs the AST
|
|
2784
|
-
linter across `unotest/e2e/**/*.js`.
|
|
3010
|
+
linter across `unotest/e2e/**/*.js`. `lint <name>` checks one scenario,
|
|
3011
|
+
named as `e2e` takes it (`lint values/string-escapes`); a file or a
|
|
3012
|
+
folder works too.
|
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/dsl": "^0.49.
|
|
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/grounder": "^0.49.
|
|
100
|
-
"@unotest/judge": "^0.49.
|
|
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
|
|
|
@@ -187,7 +187,7 @@ declare function assertText(locator: Locator, text: string | RegExp, options?: a
|
|
|
187
187
|
*
|
|
188
188
|
* The locator matches exactly expected.length elements, and element i's text matches expected[i] (string or /regex/, same rules as assertText), in order.
|
|
189
189
|
*/
|
|
190
|
-
declare function assertTexts(locator: Locator,
|
|
190
|
+
declare function assertTexts(locator: Locator, expected: unknown[], options?: any): void;
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
193
|
* assertAttribute(locator, name, expected, { exact?, not?, timeout? })
|
|
@@ -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
|
*
|
|
@@ -546,6 +560,13 @@ declare function hasAttribute(locator: Locator, name: string): boolean;
|
|
|
546
560
|
*/
|
|
547
561
|
declare function computedStyle(locator: Locator, property: string): string;
|
|
548
562
|
|
|
563
|
+
/**
|
|
564
|
+
* getImageSize(locator) → {width, height} | null
|
|
565
|
+
*
|
|
566
|
+
* Natural size of an <img>, once it has loaded or failed (waits up to the action timeout); null when it has no pixels — broken, or no src. Not an <img> fails.
|
|
567
|
+
*/
|
|
568
|
+
declare function getImageSize(locator: Locator): any;
|
|
569
|
+
|
|
549
570
|
/**
|
|
550
571
|
* hasAnimation(locator) → bool
|
|
551
572
|
*
|
|
@@ -673,11 +694,11 @@ declare function round(n: number, digits?: number): number;
|
|
|
673
694
|
declare function formatNumber(n: number, options?: any): string;
|
|
674
695
|
|
|
675
696
|
/**
|
|
676
|
-
* env(name) → string | null
|
|
697
|
+
* env(name, fallback?) → string | fallback | null
|
|
677
698
|
*
|
|
678
|
-
* 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.
|
|
679
700
|
*/
|
|
680
|
-
declare function env(name: string): any;
|
|
701
|
+
declare function env(name: string, fallback?: any): any;
|
|
681
702
|
|
|
682
703
|
/**
|
|
683
704
|
* shell(cmd, ...args, options?) → { stdout, stderr, code }
|
|
@@ -785,16 +806,16 @@ declare function apiCall(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "
|
|
|
785
806
|
declare function upload(path: string, options?: any): any;
|
|
786
807
|
|
|
787
808
|
/**
|
|
788
|
-
* locator(css)
|
|
809
|
+
* locator(css, { hasText?, hasNotText?, has?, hasNot? }?)
|
|
789
810
|
*
|
|
790
|
-
* 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.
|
|
791
812
|
*/
|
|
792
|
-
declare function locator(selector: string): Locator;
|
|
813
|
+
declare function locator(selector: string, options?: any): Locator;
|
|
793
814
|
|
|
794
815
|
/**
|
|
795
|
-
* getByRole(role, { name?, exact? })
|
|
816
|
+
* getByRole(role, { name?, exact?, checked?, disabled?, expanded?, includeHidden?, level?, pressed?, selected? })
|
|
796
817
|
*
|
|
797
|
-
* 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.
|
|
798
819
|
*/
|
|
799
820
|
declare function getByRole(role: string, options?: any): Locator;
|
|
800
821
|
|