@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.
@@ -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
- Substring match against the active page's URL.
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`. Useful after a click
848
- that triggers navigation.
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)` is refused too: once a size is set on an open page,
1232
- Playwright keeps it, and nothing hands the page back to the window. For
1233
- the real window, set `viewport: null` in the config.
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, same
1324
- argument rules (one extra → the value, two or more → ONE array) and
1325
- JSON return; a returned Promise is awaited. Not discouraged by the
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
- Multi-arg form (args destructured in the page-side function):
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: one
2115
- → the value itself, two or more → one array.
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.46.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.46.0",
89
- "@unotest/dsl": "^0.46.0",
90
- "@unotest/protocol": "^0.46.0",
91
- "@unotest/viewer": "^0.46.0",
92
- "@unotest/grounder-client": "^0.46.0"
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.46.0",
100
- "@unotest/grounder": "^0.46.0"
99
+ "@unotest/judge": "^0.48.0",
100
+ "@unotest/grounder": "^0.48.0"
101
101
  }
102
102
  }
@@ -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