@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.
@@ -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 the first `lint` / `e2e` run after a
117
- package upgrade wires it automatically (announced with one line). What it
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. Created once when missing; never rewritten silently. Type
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-op if already in the target state.
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 `null` for a name that is not set instead
1754
- of failing the step. Use it to decide:
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 a credential must stay
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. A green step prints nothing extra.
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 callback (`a.map(x => …)`), a
2585
- ternary, `+=`, `for…of`, `o.a = 2` and the like are refused by `lint`
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.1",
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.1",
89
- "@unotest/dsl": "^0.49.1",
90
- "@unotest/protocol": "^0.49.1",
91
- "@unotest/grounder-client": "^0.49.1",
92
- "@unotest/viewer": "^0.49.1"
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/judge": "^0.49.1",
100
- "@unotest/grounder": "^0.49.1"
99
+ "@unotest/grounder": "^0.49.2",
100
+ "@unotest/judge": "^0.49.2"
101
101
  }
102
102
  }
@@ -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