@unotest/web 0.46.0 → 0.48.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 +3 -2
- package/.claude/skills/write-e2e-test-ground/SKILL.md +3 -2
- package/CHANGELOG.md +113 -3
- package/README.md +5 -0
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +7 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +23 -3
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +10 -2
- package/dist/inspection/page-inject.js +12 -0
- package/dist/{interfaces-CYoL4Taq.d.ts → interfaces-CTMGjJIs.d.ts} +30 -1
- 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/dsl-reference.md +242 -19
- package/package.json +8 -8
- package/types/unotest-dsl.d.ts +58 -9
package/guides/dsl-reference.md
CHANGED
|
@@ -79,6 +79,11 @@ step.soft("label", {tag: <expr>}, () => { … });
|
|
|
79
79
|
with the next statement and records nothing — same as resuming past
|
|
80
80
|
any failure on web.
|
|
81
81
|
|
|
82
|
+
Porting a `@playwright/test` spec with several `test(...)` calls — one
|
|
83
|
+
`step.soft` per former test — and running one test over a table or a
|
|
84
|
+
JSONL file of cases are worked through in
|
|
85
|
+
[Port a @playwright/test spec](https://docs.unotest.com/guides/port-a-spec/).
|
|
86
|
+
|
|
82
87
|
The data-driven gate, in full (`readJsonLine` reads the cases; the outer
|
|
83
88
|
`step` satisfies step coverage):
|
|
84
89
|
|
|
@@ -459,9 +464,11 @@ assertAttribute(getByRole('form'), 'aria-busy', 'true', {not: true})
|
|
|
459
464
|
|
|
460
465
|
### `assertVisible(loc, options?)` / `assertHidden(loc, options?)`
|
|
461
466
|
|
|
467
|
+
**Options:** `timeout?`.
|
|
468
|
+
|
|
462
469
|
```js
|
|
463
470
|
assertVisible(getByTestId('toast'))
|
|
464
|
-
assertHidden(getByTestId('spinner'))
|
|
471
|
+
assertHidden(getByTestId('spinner'), {timeout: 10000})
|
|
465
472
|
```
|
|
466
473
|
|
|
467
474
|
### `assertNeverAppears(loc, options)`
|
|
@@ -486,6 +493,8 @@ the whole window.
|
|
|
486
493
|
|
|
487
494
|
Compares `inputValue()` (`<input>`, `<textarea>`, native `<select>`).
|
|
488
495
|
|
|
496
|
+
**Options:** `timeout?`.
|
|
497
|
+
|
|
489
498
|
```js
|
|
490
499
|
assertValue(getByLabel('Email'), 'a@b.c')
|
|
491
500
|
```
|
|
@@ -509,17 +518,25 @@ value, and so does the failure.
|
|
|
509
518
|
|
|
510
519
|
### `assertCount(loc, expected, options?)`
|
|
511
520
|
|
|
521
|
+
**Options:** `timeout?`.
|
|
522
|
+
|
|
512
523
|
```js
|
|
513
524
|
assertCount(getByRole('listitem'), 5)
|
|
525
|
+
assertCount(getByRole('row'), 20, {timeout: 15000})
|
|
514
526
|
```
|
|
515
527
|
|
|
516
528
|
### `assertUrl(pattern, options?)`
|
|
517
529
|
|
|
518
|
-
|
|
530
|
+
A string is a substring match against the active page's URL. A
|
|
531
|
+
`/regex/` is tested against the whole URL, unanchored: add `^` or `$`
|
|
532
|
+
yourself. A failure names the regex as `/source/flags`.
|
|
533
|
+
|
|
534
|
+
**Options:** `timeout?`.
|
|
519
535
|
|
|
520
536
|
```js
|
|
521
537
|
assertUrl('/dashboard')
|
|
522
538
|
assertUrl('https://example.com/checkout')
|
|
539
|
+
assertUrl(/\/cars\/\d+$/)
|
|
523
540
|
```
|
|
524
541
|
|
|
525
542
|
### `assertTrue(value, message)`
|
|
@@ -844,12 +861,16 @@ goto('/dashboard') // relative — resolved against the
|
|
|
844
861
|
|
|
845
862
|
### `waitForUrl(pattern, options?)`
|
|
846
863
|
|
|
847
|
-
Wait until the active page URL contains `pattern
|
|
848
|
-
that triggers
|
|
864
|
+
Wait until the active page URL contains `pattern` (a string) or matches
|
|
865
|
+
it (a `/regex/`, unanchored). Useful after a click that triggers
|
|
866
|
+
navigation.
|
|
867
|
+
|
|
868
|
+
**Options:** `timeout?`.
|
|
849
869
|
|
|
850
870
|
```js
|
|
851
871
|
click(getByRole('link', {name: 'Settings'}));
|
|
852
872
|
waitForUrl('/settings');
|
|
873
|
+
waitForUrl(/\/orders\/\d+/);
|
|
853
874
|
```
|
|
854
875
|
|
|
855
876
|
### `waitForNavigation(options?)`
|
|
@@ -964,6 +985,23 @@ waitForCount(locator('[data-id]'), 3, {timeout: 90000});
|
|
|
964
985
|
waitForText(/\bshipping\b/i, {timeout: 90000});
|
|
965
986
|
```
|
|
966
987
|
|
|
988
|
+
### `waitForAnimations(loc, options?)`
|
|
989
|
+
|
|
990
|
+
Wait until the element and its descendants have no animation still
|
|
991
|
+
running — a dialog done fading in, a card done sliding — before reading
|
|
992
|
+
or screenshotting it. Infinite animations (spinners) and paused ones are
|
|
993
|
+
not waited for: neither ends by itself, so waiting could only time out.
|
|
994
|
+
A static element returns at once. Same rule as `hasAnimation`.
|
|
995
|
+
|
|
996
|
+
Options: `{ timeout? }` (default 5000 ms). A timeout is an error naming
|
|
997
|
+
the element and how many animations were still running.
|
|
998
|
+
|
|
999
|
+
```js
|
|
1000
|
+
click(getByRole('button', {name: 'Edit'}));
|
|
1001
|
+
waitForAnimations(getByRole('dialog'));
|
|
1002
|
+
assertScreenshot(getByRole('dialog'), 'edit-dialog');
|
|
1003
|
+
```
|
|
1004
|
+
|
|
967
1005
|
### `pause(ms)`
|
|
968
1006
|
|
|
969
1007
|
Hard sleep. **Avoid** — the linter flags it (`lint:pause-explicit`). Use
|
|
@@ -1228,9 +1266,34 @@ Both fields are required. Anything else is refused on that line,
|
|
|
1228
1266
|
and changing it would need a new browser context — the page under test
|
|
1229
1267
|
would be lost. See [Viewport and scale](#viewport-and-scale).
|
|
1230
1268
|
|
|
1231
|
-
`setViewport(null)`
|
|
1232
|
-
|
|
1233
|
-
|
|
1269
|
+
`setViewport(null)` runs the test on the real window — but only before
|
|
1270
|
+
the test does anything in its browser: the first navigation, `mockRoute`,
|
|
1271
|
+
a cookie or storage value, the page clock, `emulateMedia`, a second page
|
|
1272
|
+
or context. It opens the browser context again with the same options;
|
|
1273
|
+
after any of those it is refused, since the new context would lose them.
|
|
1274
|
+
On a browser profile (`launch.userDataDir`, `launch.profile`) it is
|
|
1275
|
+
refused always. For a whole suite, set `viewport: null` in the config.
|
|
1276
|
+
|
|
1277
|
+
### `emulateMedia({reducedMotion?, colorScheme?})`
|
|
1278
|
+
|
|
1279
|
+
Change the media features the current tab reports, mid-scenario — what
|
|
1280
|
+
`matchMedia` and CSS `@media` see from that moment, without a reload.
|
|
1281
|
+
`reducedMotion`: `'reduce'` | `'no-preference'`; `colorScheme`:
|
|
1282
|
+
`'light'` | `'dark'` | `'no-preference'`. A feature left out keeps its
|
|
1283
|
+
current value. An unknown key, another value or an empty object is
|
|
1284
|
+
refused on that line.
|
|
1285
|
+
|
|
1286
|
+
```js
|
|
1287
|
+
emulateMedia({reducedMotion: 'reduce'}); // the app's no-animation path
|
|
1288
|
+
click(getByRole('button', {name: 'Edit'}));
|
|
1289
|
+
assertScreenshot(getByRole('dialog'), 'edit-dialog-still');
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
It applies to **this tab only**: a tab or popup opened afterwards — even
|
|
1293
|
+
by the same browser context — starts without it; call `emulateMedia`
|
|
1294
|
+
again after `setPage` / `waitForPage`. An app that reads the color
|
|
1295
|
+
scheme once at load sees the change on its next load (`reload()`), not
|
|
1296
|
+
before.
|
|
1234
1297
|
|
|
1235
1298
|
#### Viewport and scale
|
|
1236
1299
|
|
|
@@ -1320,9 +1383,10 @@ goto('chrome-extension://' + id + '/popup.html');
|
|
|
1320
1383
|
`chrome-extension://<id>/sw.js`. A running worker comes back at once;
|
|
1321
1384
|
otherwise it waits up to `timeoutMs` (default: the navigation timeout).
|
|
1322
1385
|
`url` — a substring or `/regex/` when several extensions run.
|
|
1323
|
-
- `evaluateInServiceWorker(js, ...args)` — `evaluate` in the worker
|
|
1324
|
-
|
|
1325
|
-
|
|
1386
|
+
- `evaluateInServiceWorker(js, ...args)` — `evaluate` in the worker. The
|
|
1387
|
+
extras reach the body by the same rule as `evaluate`: none →
|
|
1388
|
+
`undefined`, one → the value itself, two or more → one array. JSON
|
|
1389
|
+
return; a returned Promise is awaited. Not discouraged by the
|
|
1326
1390
|
linter: `chrome.*` has no other way in.
|
|
1327
1391
|
- `extensionId()` — the id out of the worker's URL.
|
|
1328
1392
|
- MV3 stops an idle worker after ~30 s. Each call finds the worker anew,
|
|
@@ -1863,6 +1927,10 @@ mockRoute('**/ads/**', { abort: true });
|
|
|
1863
1927
|
navigation to your site still loads. Only the document: its scripts,
|
|
1864
1928
|
styles, images and API calls are ordinary requests, and an abort mask
|
|
1865
1929
|
that matches them stops them.
|
|
1930
|
+
- A mask reaches web requests only: http(s) and ws(s). An extension's
|
|
1931
|
+
own pages and scripts (`chrome-extension://`) load under `**/*`, so
|
|
1932
|
+
a network allowlist does not leave its UI dead. To mock one of them,
|
|
1933
|
+
start the mask with its scheme: `chrome-extension://<id>/panel.js`.
|
|
1866
1934
|
|
|
1867
1935
|
A network allowlist is two masks — the later one wins where both match.
|
|
1868
1936
|
The second one lets through everything your app loads from its own
|
|
@@ -1957,6 +2025,29 @@ box = boundingBox(getByRole('dialog'));
|
|
|
1957
2025
|
assertTrue(box.width <= 480, json(box));
|
|
1958
2026
|
```
|
|
1959
2027
|
|
|
2028
|
+
### `computedStyle(loc, property)` → `string`
|
|
2029
|
+
|
|
2030
|
+
The element's computed CSS value, as the browser resolved it — what
|
|
2031
|
+
`getComputedStyle(el).getPropertyValue(property)` gives, trimmed.
|
|
2032
|
+
Custom properties read too, inherited ones included; a property that is
|
|
2033
|
+
not set reads `""`. A missing or ambiguous element fails as
|
|
2034
|
+
`getAttribute` does.
|
|
2035
|
+
|
|
2036
|
+
```js
|
|
2037
|
+
assertTrue(computedStyle(getByRole('dialog'), 'opacity') == 1, 'dialog is faded');
|
|
2038
|
+
accent = computedStyle(locator('body'), '--accent');
|
|
2039
|
+
```
|
|
2040
|
+
|
|
2041
|
+
### `hasAnimation(loc)` → `boolean`
|
|
2042
|
+
|
|
2043
|
+
Whether the element or a descendant has an animation still running
|
|
2044
|
+
towards an end. Infinite (spinners) and paused animations do not count —
|
|
2045
|
+
the rule `waitForAnimations` waits by.
|
|
2046
|
+
|
|
2047
|
+
```js
|
|
2048
|
+
assertTrue(hasAnimation(getByRole('dialog')) === false, 'dialog still moving');
|
|
2049
|
+
```
|
|
2050
|
+
|
|
1960
2051
|
### `getTitle()` → `string`
|
|
1961
2052
|
|
|
1962
2053
|
Active page title.
|
|
@@ -2047,6 +2138,60 @@ src = getAttribute(locator('iframe#chat'), 'src');
|
|
|
2047
2138
|
assertEqual(urlParam(src, 'roomId'), 'r-7');
|
|
2048
2139
|
```
|
|
2049
2140
|
|
|
2141
|
+
### `toNumber(text, options?)` → `number`
|
|
2142
|
+
|
|
2143
|
+
A number as a page writes it, for arithmetic and comparison. What
|
|
2144
|
+
`parseInt`, `parseFloat` and `Number` would be in JS.
|
|
2145
|
+
|
|
2146
|
+
- Spaces group digits, NBSP and the narrow NBSP too: `'1 234 567'`.
|
|
2147
|
+
- A leading sign: `-`, `+` or the typographic minus `−`.
|
|
2148
|
+
- With both `,` and `.`, the last one is the decimal mark:
|
|
2149
|
+
`'1,234.56'` and `'1.234,56'` are both 1234.56. One mark used twice
|
|
2150
|
+
groups: `'1,234,567'`. One mark used once is the decimal mark:
|
|
2151
|
+
`'12,5'`, `'0.125'`.
|
|
2152
|
+
- `'1,234'` and `'1.234'` read both ways (1234 or 1.234), so they fail
|
|
2153
|
+
the step unless `{decimal: ','}` or `{decimal: '.'}` says which. With
|
|
2154
|
+
`{decimal}` the other mark may only group digits left of it.
|
|
2155
|
+
- Anything else fails the step and quotes the input: units and
|
|
2156
|
+
currency (`'440px'`, `'€12'`), exponents, an empty string. Cut the
|
|
2157
|
+
number out first with `textMatches`.
|
|
2158
|
+
- A number passes through unchanged.
|
|
2159
|
+
|
|
2160
|
+
```js
|
|
2161
|
+
total = toNumber(getInnerText(getByTestId('total'))); // "1 234,50"
|
|
2162
|
+
assertTrue(total > 1000, 'total: ' + total);
|
|
2163
|
+
width = toNumber(textMatches(computedStyle(phone, '--phone-w'), /(\d+)px/)[1]);
|
|
2164
|
+
```
|
|
2165
|
+
|
|
2166
|
+
### `round(n, digits?)` → `number`
|
|
2167
|
+
|
|
2168
|
+
Round to `digits` decimals, 0 to 10, default 0. Half goes away from
|
|
2169
|
+
zero, on the decimal as written: `round(2.5)` is 3, `round(-2.5)` is
|
|
2170
|
+
-3, `round(1.005, 2)` is 1.01. A string fails: read it with
|
|
2171
|
+
`toNumber` first.
|
|
2172
|
+
|
|
2173
|
+
```js
|
|
2174
|
+
payout = round(stake * odds, 2);
|
|
2175
|
+
```
|
|
2176
|
+
|
|
2177
|
+
### `formatNumber(n, options?)` → `string`
|
|
2178
|
+
|
|
2179
|
+
A number as text, with digit grouping, through `Intl.NumberFormat`.
|
|
2180
|
+
|
|
2181
|
+
**Options:** `locale?` (default `'en-US'`), `minFractionDigits?`,
|
|
2182
|
+
`maxFractionDigits?` (0 to 20; without them, up to 3 decimals).
|
|
2183
|
+
|
|
2184
|
+
- Without a locale the answer is the same on every machine, whatever
|
|
2185
|
+
its system language: `formatNumber(1234.5)` is `'1,234.5'`.
|
|
2186
|
+
- With another locale the text comes from the ICU data of the Node
|
|
2187
|
+
that runs the scenario, spaces included (`fr-FR` puts a narrow NBSP
|
|
2188
|
+
between thousands). Compare it with what the page shows rather than
|
|
2189
|
+
with a string typed in the scenario.
|
|
2190
|
+
|
|
2191
|
+
```js
|
|
2192
|
+
assertText(getByTestId('payout'), formatNumber(payout, {minFractionDigits: 2, maxFractionDigits: 2}));
|
|
2193
|
+
```
|
|
2194
|
+
|
|
2050
2195
|
### `json(value)` → `string`
|
|
2051
2196
|
|
|
2052
2197
|
Serialize any value to a JSON string. The debug tool for API and data
|
|
@@ -2098,12 +2243,16 @@ getters or the action API.
|
|
|
2098
2243
|
height = evaluate(`document.documentElement.scrollHeight`);
|
|
2099
2244
|
```
|
|
2100
2245
|
|
|
2101
|
-
|
|
2246
|
+
The extra arguments reach the body by one rule: none → `undefined`,
|
|
2247
|
+
exactly one → the value itself, two or more → ONE array the body
|
|
2248
|
+
destructures. So one argument is `function(x)`, not `function([x])` —
|
|
2249
|
+
that one fails with `2 is not iterable`.
|
|
2102
2250
|
|
|
2103
2251
|
```js
|
|
2252
|
+
doubled = evaluate(`function(n) { return n * 2; }`, 21); // one → the value
|
|
2104
2253
|
result = evaluate(
|
|
2105
2254
|
`function([a, b]) { return window.myApp.compute(a, b); }`,
|
|
2106
|
-
10, 20
|
|
2255
|
+
10, 20 // two → one array
|
|
2107
2256
|
);
|
|
2108
2257
|
```
|
|
2109
2258
|
|
|
@@ -2111,14 +2260,25 @@ Backticks are **raw** — `${…}` interpolation is rejected at parse time
|
|
|
2111
2260
|
(see `vendor/dsl/EXTENSIONS.md`).
|
|
2112
2261
|
|
|
2113
2262
|
`evaluateOnLocator(loc, js, ...args)` — the same hatch on one element.
|
|
2114
|
-
The body gets the element first, then the extras by the same rule
|
|
2115
|
-
→ the value itself, two or more → one
|
|
2263
|
+
The body gets the element first, then the extras by the same rule as
|
|
2264
|
+
`evaluate`: none → `undefined`, one → the value itself, two or more → one
|
|
2265
|
+
array.
|
|
2116
2266
|
|
|
2117
2267
|
```js
|
|
2118
|
-
color = evaluateOnLocator(getByRole('alert'), `(el) => getComputedStyle(el).color`);
|
|
2119
2268
|
ok = evaluateOnLocator(getByRole('main'), `(el, [a, b]) => el.children.length > a + b`, 1, 2);
|
|
2120
2269
|
```
|
|
2121
2270
|
|
|
2271
|
+
Reach for it only for what no typed call reads — a style is
|
|
2272
|
+
`computedStyle`, a box is `boundingBox`. Overflow is one such case: is
|
|
2273
|
+
the text cut off? There is no getter for `scrollWidth`, so the line says
|
|
2274
|
+
why it uses the hatch, and the linter takes the reason:
|
|
2275
|
+
|
|
2276
|
+
```js
|
|
2277
|
+
// Truncated with an ellipsis: the content is wider than the cell.
|
|
2278
|
+
overflows = evaluateOnLocator(getByRole('cell', {name: /^R-00001/}), `(el) => el.scrollWidth > el.clientWidth`); // lint-ok: no typed getter reads scrollWidth
|
|
2279
|
+
assertTrue(overflows === false, 'the id is cut off');
|
|
2280
|
+
```
|
|
2281
|
+
|
|
2122
2282
|
---
|
|
2123
2283
|
|
|
2124
2284
|
## Diagnostics
|
|
@@ -2288,7 +2448,8 @@ The DSL is JavaScript-shaped but parsed by a small AST engine
|
|
|
2288
2448
|
- **Assignments** — `name = expression;` (no `let`/`const`/`var`).
|
|
2289
2449
|
- **`if` / `else`** and **`for` in the canonical shape**
|
|
2290
2450
|
`for (i = 0; i < N; i = i + 1) { … }` (increment must be an
|
|
2291
|
-
assignment — `i++` is rejected everywhere).
|
|
2451
|
+
assignment — `i++` is rejected everywhere). A loop over cases, each a
|
|
2452
|
+
tagged soft step: [Port a @playwright/test spec](https://docs.unotest.com/guides/port-a-spec/).
|
|
2292
2453
|
- **`break`** — inside a loop body only. Typical polling loop:
|
|
2293
2454
|
|
|
2294
2455
|
```js
|
|
@@ -2308,17 +2469,20 @@ The DSL is JavaScript-shaped but parsed by a small AST engine
|
|
|
2308
2469
|
- **Index access on data** — `docs[0]`, `docs[0].objectsCount`,
|
|
2309
2470
|
`res.body.items[0].id`: see [Strings, objects, indexes](#strings-objects-indexes).
|
|
2310
2471
|
Indexing a **Locator** is rejected — disambiguate with chain methods
|
|
2311
|
-
(`.filter(...)`), and `arr[i] = v` assignment stays unsupported
|
|
2472
|
+
(`.filter(...)`), and `arr[i] = v` assignment stays unsupported —
|
|
2473
|
+
`arr.push(v)` appends: see [Logical not and arrays](#logical-not-and-arrays).
|
|
2312
2474
|
- **Object literals** — as call arguments (`click(loc, {force: true})`)
|
|
2313
2475
|
and as values in a variable: see [Strings, objects, indexes](#strings-objects-indexes).
|
|
2314
2476
|
- **Member access** — `row.email` (dot form; `obj['key']` doesn't
|
|
2315
2477
|
parse).
|
|
2316
2478
|
- **Method chains** — `getByRole('row').filter({hasText:'X'}).click()`.
|
|
2317
2479
|
- **Backticks** — multi-line raw strings, no `${}` interpolation.
|
|
2318
|
-
- **Booleans, numbers, strings, `null
|
|
2480
|
+
- **Booleans, numbers, strings, `null`** — escapes in strings: see
|
|
2481
|
+
[Escape rules](#escape-rules).
|
|
2319
2482
|
- **Comparison operators** — `==` / `!=` (loose: `"1" == 1` is true,
|
|
2320
2483
|
numeric strings coerce), `===` / `!==` (strict: type AND value must
|
|
2321
|
-
match, `1 === "1"` is false), `<` `<=` `>` `>=`, logical `&&` /
|
|
2484
|
+
match, `1 === "1"` is false), `<` `<=` `>` `>=`, logical `&&` / `||`
|
|
2485
|
+
and `!` (see [Logical not and arrays](#logical-not-and-arrays)).
|
|
2322
2486
|
Caveat: DOM reads (`textContent`, `getInputValue`, `getAttribute`)
|
|
2323
2487
|
return **strings** — for comparing against a number use `==`
|
|
2324
2488
|
(`getAttribute(loc, "data-count") === 5` is always false; write
|
|
@@ -2336,6 +2500,30 @@ inspection.
|
|
|
2336
2500
|
See `vendor/dsl/EXTENSIONS.md` for the
|
|
2337
2501
|
exact grammar deltas from the upstream parser.
|
|
2338
2502
|
|
|
2503
|
+
### Escape rules
|
|
2504
|
+
|
|
2505
|
+
In a quoted string (`'…'` or `"…"`):
|
|
2506
|
+
|
|
2507
|
+
| Escape | Gives |
|
|
2508
|
+
|---|---|
|
|
2509
|
+
| `\n` `\t` `\r` `\f` `\v` | the control character |
|
|
2510
|
+
| `\\` | one backslash |
|
|
2511
|
+
| `\'` in `'…'`, `\"` in `"…"` | the quote |
|
|
2512
|
+
| `\0` (no digit after it) | the NUL character |
|
|
2513
|
+
| `\xHH` | the character with that hex code: `"\xA0"` is a non-breaking space |
|
|
2514
|
+
| `\uXXXX` | the same with four hex digits: `'\u00A0'` |
|
|
2515
|
+
|
|
2516
|
+
A malformed `\x` / `\u` (`'\u12'`, `'\xG1'`) is a parse error at the
|
|
2517
|
+
backslash. Any other backslash pair stays **as written** — `'\d+'` is
|
|
2518
|
+
three characters, which is what a regex built from a string needs. The
|
|
2519
|
+
same goes for `\b`: a word boundary there, never a backspace. `\01` and
|
|
2520
|
+
the other quote (`"\'"`) are kept as written too. Backtick strings are raw: no
|
|
2521
|
+
escape decodes inside `` `…` ``.
|
|
2522
|
+
|
|
2523
|
+
```js
|
|
2524
|
+
assertText(getByRole('cell', {name: /^Total/}), '1\u00A0250'); // NBSP between digit groups
|
|
2525
|
+
```
|
|
2526
|
+
|
|
2339
2527
|
### Strings, objects, indexes
|
|
2340
2528
|
|
|
2341
2529
|
Since 0.44.0.
|
|
@@ -2385,6 +2573,41 @@ assertTrue(res.body.items[0].status == 'ready', json(res.body));
|
|
|
2385
2573
|
first = dbQuery('SELECT id FROM users ORDER BY id')[0].id;
|
|
2386
2574
|
```
|
|
2387
2575
|
|
|
2576
|
+
### Logical not and arrays
|
|
2577
|
+
|
|
2578
|
+
Since 0.48.0.
|
|
2579
|
+
|
|
2580
|
+
**`!x`** negates, as in JS: the result is always `true` or `false`, and
|
|
2581
|
+
`0`, `""`, `null` and `false` are the falsy values — the same ones `if`
|
|
2582
|
+
treats as false. An array or an object is truthy, empty or not. `!`
|
|
2583
|
+
binds tighter than any other operator: `!a && b` is `(!a) && b`.
|
|
2584
|
+
|
|
2585
|
+
```js
|
|
2586
|
+
if (!isVisible(getByRole('dialog'))) { log('dialog closed'); }
|
|
2587
|
+
assertTrue(!out.stdout.includes('error'), out.stdout);
|
|
2588
|
+
```
|
|
2589
|
+
|
|
2590
|
+
**`arr.push(x, …)`** appends to the array in place and returns its new
|
|
2591
|
+
length. Other array methods (`pop`, `map`, `join`, …) are not there;
|
|
2592
|
+
`length` is.
|
|
2593
|
+
|
|
2594
|
+
```js
|
|
2595
|
+
rows = getByRole('row');
|
|
2596
|
+
ids = [];
|
|
2597
|
+
for (i = 0; i < count(rows); i = i + 1) {
|
|
2598
|
+
ids.push(textContent(rows.nth(i)));
|
|
2599
|
+
}
|
|
2600
|
+
assertTrue(ids.length > 0, json(ids));
|
|
2601
|
+
```
|
|
2602
|
+
|
|
2603
|
+
A variable holds the array itself, not a copy, as in JS: after
|
|
2604
|
+
`b = a; a.push(1)`, `b` has the new element too. So does a caller whose
|
|
2605
|
+
array a helper pushed onto. A helper that assigns a new array to its
|
|
2606
|
+
parameter (`list = []`) changes only its own name. `push` on a value
|
|
2607
|
+
that is not an array — a string, a number, a `null` field — fails the
|
|
2608
|
+
step with that type in the message; on a string the linter flags it
|
|
2609
|
+
first.
|
|
2610
|
+
|
|
2388
2611
|
### `teardown` — give the stand back
|
|
2389
2612
|
|
|
2390
2613
|
Since 0.44.0.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
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.
|
|
89
|
-
"@unotest/
|
|
90
|
-
"@unotest/
|
|
91
|
-
"@unotest/
|
|
92
|
-
"@unotest/
|
|
88
|
+
"@unotest/core": "^0.48.0",
|
|
89
|
+
"@unotest/protocol": "^0.48.0",
|
|
90
|
+
"@unotest/dsl": "^0.48.0",
|
|
91
|
+
"@unotest/grounder-client": "^0.48.0",
|
|
92
|
+
"@unotest/viewer": "^0.48.0"
|
|
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.
|
|
100
|
-
"@unotest/grounder": "^0.
|
|
99
|
+
"@unotest/judge": "^0.48.0",
|
|
100
|
+
"@unotest/grounder": "^0.48.0"
|
|
101
101
|
}
|
|
102
102
|
}
|
package/types/unotest-dsl.d.ts
CHANGED
|
@@ -201,14 +201,14 @@ declare function assertAttribute(locator: Locator, name: string, value: string |
|
|
|
201
201
|
*
|
|
202
202
|
* Element is visible.
|
|
203
203
|
*/
|
|
204
|
-
declare function assertVisible(locator: Locator): void;
|
|
204
|
+
declare function assertVisible(locator: Locator, options?: any): void;
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
207
|
* assertHidden(locator, { timeout? })
|
|
208
208
|
*
|
|
209
209
|
* Element is hidden or detached.
|
|
210
210
|
*/
|
|
211
|
-
declare function assertHidden(locator: Locator): void;
|
|
211
|
+
declare function assertHidden(locator: Locator, options?: any): void;
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* assertNeverAppears(locator, { withinMs })
|
|
@@ -222,21 +222,21 @@ declare function assertNeverAppears(locator: Locator, options: any): void;
|
|
|
222
222
|
*
|
|
223
223
|
* Input's value equals `expected`. On a checkbox/radio/switch: its checked state — pass 'true' or 'false'.
|
|
224
224
|
*/
|
|
225
|
-
declare function assertValue(locator: Locator, value: string): void;
|
|
225
|
+
declare function assertValue(locator: Locator, value: string, options?: any): void;
|
|
226
226
|
|
|
227
227
|
/**
|
|
228
228
|
* assertCount(locator, expected, { timeout? })
|
|
229
229
|
*
|
|
230
230
|
* Number of matches equals `expected`.
|
|
231
231
|
*/
|
|
232
|
-
declare function assertCount(locator: Locator, count: number): void;
|
|
232
|
+
declare function assertCount(locator: Locator, count: number, options?: any): void;
|
|
233
233
|
|
|
234
234
|
/**
|
|
235
235
|
* assertUrl(pattern, { timeout? })
|
|
236
236
|
*
|
|
237
|
-
* Current URL contains `pattern
|
|
237
|
+
* Current URL contains `pattern` (a string) or matches it (a /regex/, unanchored). A failure names the pattern as /source/flags.
|
|
238
238
|
*/
|
|
239
|
-
declare function assertUrl(pattern: string): void;
|
|
239
|
+
declare function assertUrl(pattern: string | RegExp, options?: any): void;
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
242
|
* assertJudge(locator, rubric)
|
|
@@ -413,6 +413,13 @@ declare function closePage(index?: number): void;
|
|
|
413
413
|
*/
|
|
414
414
|
declare function setViewport(size: any): void;
|
|
415
415
|
|
|
416
|
+
/**
|
|
417
|
+
* emulateMedia({ reducedMotion?, colorScheme? })
|
|
418
|
+
*
|
|
419
|
+
* Change the media features the current tab reports from now on — what matchMedia and CSS @media see: reducedMotion 'reduce' | 'no-preference', colorScheme 'light' | 'dark' | 'no-preference'. This tab only: a tab or popup opened later starts without it.
|
|
420
|
+
*/
|
|
421
|
+
declare function emulateMedia(media: any): void;
|
|
422
|
+
|
|
416
423
|
/**
|
|
417
424
|
* newContext() → number
|
|
418
425
|
*
|
|
@@ -472,9 +479,9 @@ declare function goForward(): void;
|
|
|
472
479
|
/**
|
|
473
480
|
* waitForUrl(pattern, { timeout? })
|
|
474
481
|
*
|
|
475
|
-
* Wait until the URL contains `pattern` (substring match).
|
|
482
|
+
* Wait until the URL contains `pattern` (a string, substring match) or matches it (a /regex/, unanchored).
|
|
476
483
|
*/
|
|
477
|
-
declare function waitForUrl(pattern: string): void;
|
|
484
|
+
declare function waitForUrl(pattern: string | RegExp, options?: any): void;
|
|
478
485
|
|
|
479
486
|
/**
|
|
480
487
|
* waitForNavigation({ timeout? })
|
|
@@ -532,6 +539,20 @@ declare function isEnabled(locator: Locator): boolean;
|
|
|
532
539
|
*/
|
|
533
540
|
declare function hasAttribute(locator: Locator, name: string): boolean;
|
|
534
541
|
|
|
542
|
+
/**
|
|
543
|
+
* computedStyle(locator, property) → string
|
|
544
|
+
*
|
|
545
|
+
* The element's computed CSS value, as the browser resolved it: computedStyle(card, "opacity") → "0.5", custom properties too ("--accent"). "" when the property is not set.
|
|
546
|
+
*/
|
|
547
|
+
declare function computedStyle(locator: Locator, property: string): string;
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* hasAnimation(locator) → bool
|
|
551
|
+
*
|
|
552
|
+
* Whether the element or a descendant has an animation still running towards an end. Infinite (spinners) and paused ones do not count — the same rule waitForAnimations waits by.
|
|
553
|
+
*/
|
|
554
|
+
declare function hasAnimation(locator: Locator): boolean;
|
|
555
|
+
|
|
535
556
|
/**
|
|
536
557
|
* randomFirstName() → string
|
|
537
558
|
*
|
|
@@ -630,6 +651,27 @@ declare function parseJson(text: string): any;
|
|
|
630
651
|
*/
|
|
631
652
|
declare function urlParam(url: string, key: string): any;
|
|
632
653
|
|
|
654
|
+
/**
|
|
655
|
+
* toNumber(text, { decimal? }) → number
|
|
656
|
+
*
|
|
657
|
+
* A number as a page writes it: '1 234,5', '1,234.56', '−12'. Spaces (NBSP too) group digits; with both ',' and '.' the last one is the decimal mark; one mark used twice groups. '1,234' and '1.234' read both ways and FAIL unless {decimal: ',' | '.'} says which. Units and currency ('440px', '€12') FAIL, quoting the input — cut the number out with textMatches first. A number passes through.
|
|
658
|
+
*/
|
|
659
|
+
declare function toNumber(text: any, options?: any): number;
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* round(n, digits?) → number
|
|
663
|
+
*
|
|
664
|
+
* Round to `digits` decimals (0..10, default 0), half away from zero on the decimal as written: round(2.5) → 3, round(-2.5) → -3, round(1.005, 2) → 1.01.
|
|
665
|
+
*/
|
|
666
|
+
declare function round(n: number, digits?: number): number;
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* formatNumber(n, { locale?, minFractionDigits?, maxFractionDigits? }) → string
|
|
670
|
+
*
|
|
671
|
+
* A number as text, via Intl.NumberFormat with digit grouping. Without a locale it is en-US on every machine: formatNumber(1234.5) → '1,234.5'. Another locale's output (its spaces and marks) comes from the ICU of the Node that runs it.
|
|
672
|
+
*/
|
|
673
|
+
declare function formatNumber(n: number, options?: any): string;
|
|
674
|
+
|
|
633
675
|
/**
|
|
634
676
|
* env(name) → string | null
|
|
635
677
|
*
|
|
@@ -903,6 +945,13 @@ declare function waitForText(text: string | RegExp, options?: any): void;
|
|
|
903
945
|
*/
|
|
904
946
|
declare function waitForCount(locator: Locator, count: number, options?: any): void;
|
|
905
947
|
|
|
948
|
+
/**
|
|
949
|
+
* waitForAnimations(locator, { timeout? })
|
|
950
|
+
*
|
|
951
|
+
* Wait until the element and its descendants have no animation still running — a dialog done fading in, a card done sliding. Infinite ones (spinners) and paused ones are not waited for. Default timeout 5000 ms; a timeout names the element and how many were left.
|
|
952
|
+
*/
|
|
953
|
+
declare function waitForAnimations(locator: Locator, options?: any): void;
|
|
954
|
+
|
|
906
955
|
/**
|
|
907
956
|
* pause(ms)
|
|
908
957
|
*
|
|
@@ -941,7 +990,7 @@ declare function clockRun(): void;
|
|
|
941
990
|
/**
|
|
942
991
|
* mockRoute(glob, response)
|
|
943
992
|
*
|
|
944
|
-
* From now on, requests matching `glob` (a Playwright URL glob: `**\/api/items*`) get a fixed answer: `{status?, body?, headers?, contentType?}` (an object body goes as JSON), `{abort: true, record?}` or `{continue: true}` (to the network). Where several masks match, the one mocked last answers; mocking a mask again replaces its answer. A response to `**\/*` is refused; the site's own page is never aborted. Mocks last until the scenario ends.
|
|
993
|
+
* From now on, requests matching `glob` (a Playwright URL glob: `**\/api/items*`) get a fixed answer: `{status?, body?, headers?, contentType?}` (an object body goes as JSON), `{abort: true, record?}` or `{continue: true}` (to the network). Where several masks match, the one mocked last answers; mocking a mask again replaces its answer. A response to `**\/*` is refused; the site's own page is never aborted. A mask reaches http(s) and ws(s) only, so an extension's own pages and scripts load under `**\/*`; to mock them, start the mask with their scheme (`chrome-extension://<id>/x.js`). Mocks last until the scenario ends.
|
|
945
994
|
*/
|
|
946
995
|
declare function mockRoute(glob: string, response: any): void;
|
|
947
996
|
|