@unotest/web 0.45.0 → 0.46.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.
@@ -198,6 +198,10 @@ Actions, navigation, waits and assertions take their time limit in
198
198
  milliseconds as `timeout` (`timeoutMs` is accepted too, the name the
199
199
  newer functions use); giving both in one call is refused.
200
200
 
201
+ An option an action, a navigation, a wait or an assertion does not take
202
+ is refused with the ones it does, so a misspelled `{timout: 5000}` fails at once
203
+ instead of waiting the default.
204
+
201
205
  ---
202
206
 
203
207
  ## Builders
@@ -405,15 +409,54 @@ override per-call with `{timeout}`). Throw `AssertionError` on failure.
405
409
 
406
410
  ### `assertText(loc, expected, options?)`
407
411
 
408
- Default is exact match. Use `{exact: false}` for substring.
412
+ `expected` is a string or a `/regex/`. A string must equal the element's
413
+ text; with `{exact: false}` it only has to occur in it. A regex is tested
414
+ against the text as is: `exact` does not anchor it, write `^…$` for that.
409
415
 
410
416
  ```js
411
417
  assertText(getByTestId('success-banner'), 'Saved successfully')
412
418
  assertText(getByRole('heading'), 'Welcome', {exact: false})
419
+ assertText(getByTestId('order-id'), /^#\d{4}$/)
420
+ assertText(getByRole('status'), 'Saving…', {not: true}) // waits until it changes
421
+ ```
422
+
423
+ - `{not: true}` inverts the check and waits for it: the step passes as
424
+ soon as the text no longer matches, and fails at the timeout with the
425
+ text that still does.
426
+ - Empty text: `assertText(loc, '')`. Any text at all:
427
+ `assertText(loc, '', {not: true})`.
428
+
429
+ **Options:** `timeout?`, `exact?` (default `true`), `not?`.
430
+
431
+ ### `assertTexts(loc, expected, options?)`
432
+
433
+ One entry per element, in order: the locator must match exactly
434
+ `expected.length` elements, and element `i` must match `expected[i]`
435
+ (string or regex, the rules of `assertText`). Polled as a whole, so a
436
+ list that is still filling in passes once it is complete.
437
+
438
+ ```js
439
+ assertTexts(getByRole('listitem'), ['Apple', 'Banana', /^Cherry/])
413
440
  ```
414
441
 
415
442
  **Options:** `timeout?`, `exact?` (default `true`).
416
443
 
444
+ ### `assertAttribute(loc, name, expected, options?)`
445
+
446
+ Polls the attribute `name` until it matches `expected`: a string equals
447
+ it (`{exact: false}` — contains), a regex is tested against it. An
448
+ absent attribute matches nothing; with `{not: true}` the step passes
449
+ once the value stops matching or the attribute is gone.
450
+
451
+ ```js
452
+ click(getByRole('button', {name: 'Menu'}));
453
+ assertAttribute(getByRole('button', {name: 'Menu'}), 'aria-expanded', 'true')
454
+ assertAttribute(getByRole('link', {name: 'Docs'}), 'href', /\/docs\//)
455
+ assertAttribute(getByRole('form'), 'aria-busy', 'true', {not: true})
456
+ ```
457
+
458
+ **Options:** `timeout?`, `exact?` (default `true`), `not?`.
459
+
417
460
  ### `assertVisible(loc, options?)` / `assertHidden(loc, options?)`
418
461
 
419
462
  ```js
@@ -501,6 +544,75 @@ assertEqual(body.controls, {theme: 'dark', devices: ['pixel']});
501
544
  // assertEqual: values differ at devices[0]: expected "pixel", got "iphone"
502
545
  ```
503
546
 
547
+ ### `assertSchema(value, schema)`
548
+
549
+ Check a value against a JSON Schema (draft 2020-12) — typically one your
550
+ app generates from its own zod schemas (`z.toJSONSchema`) and the scenario
551
+ reads with `readJson`. The failure names every field that breaks the
552
+ schema, by path, with what is wrong there:
553
+
554
+ ```js
555
+ schema = readJson('unotest/fixtures/message.schema.json');
556
+ req = waitForRequest('**/api/messages');
557
+ assertSchema(req.postDataJson, schema);
558
+ // assertSchema: the value breaks the schema in 2 places:
559
+ // /room: must be >= 1
560
+ // /tags/1: must be string
561
+ ```
562
+
563
+ - A `$ref` inside the schema (`#/$defs/tag`) is followed; a `$ref` to
564
+ another file is refused. Put shared parts under `$defs`.
565
+ - A schema that declares another draft in `$schema`, or is itself
566
+ invalid, is refused before any value is checked.
567
+ - `format` (`email`, `uuid`, `date-time`) is not checked.
568
+ - The schema must be an object: pass what `readJson` returned, not the
569
+ file name.
570
+
571
+ ### `skip(reason, condition?)`
572
+
573
+ End the test as **skipped**: it does not apply here — a live-only check
574
+ with no live target configured, data this stand does not have. With the
575
+ condition true, or without one, the run reports skipped with the reason,
576
+ neither passed nor failed; nothing after the call runs, and `teardown(...)`
577
+ blocks registered before it still play. With the condition false the test
578
+ goes on.
579
+
580
+ `skip` is a test guard: it stands in the `test_*` body next to its steps,
581
+ like `step(...)` and `teardown(...)`, not inside one.
582
+
583
+ ```js
584
+ function test_live_payment() {
585
+ skip('LIVE_API is not set: no live target', env('LIVE_API') == null);
586
+ step("Pay on the live API", () => {
587
+ // …
588
+ });
589
+ }
590
+ ```
591
+
592
+ A decision on data the test reads goes after the step that reads it:
593
+
594
+ ```js
595
+ function test_refund() {
596
+ step("Find a paid order", () => {
597
+ orders = apiCall('GET', '/orders?status=paid');
598
+ });
599
+ skip('no paid order on this stand', orders.length == 0);
600
+ // …
601
+ }
602
+ ```
603
+
604
+ - The CLI prints `○ test: … skipped: <reason>` and counts it apart:
605
+ `0 passed, 0 failed, 0 errored, 1 skipped`. The exit code is 0.
606
+ - A collection counts it as `skipped=N(test=N)`.
607
+ - `run_test` answers `outcome: "skipped"` with `skipReason`.
608
+ - A skip after a failed `step.soft(...)` is a failed test, and so is a
609
+ skipped test whose teardown fails.
610
+ - The condition is `true` or `false`; anything else fails the test with
611
+ the value named — compare it: `env('LIVE_API') == null`.
612
+ - Call it at the level of the `test_*` body: inside a step or an `if`
613
+ block, in a helper or a `flow_*`, it is reported by
614
+ `lint:skip-outside-test`.
615
+
504
616
  ### `assertJudge(loc, rubric)`
505
617
 
506
618
  LLM-judge assert for **free-form text** — chat replies, generated
@@ -1086,7 +1198,8 @@ own.
1086
1198
  - **Traces and video:** not recorded for any context — unotest does not
1087
1199
  capture Playwright traces or video today.
1088
1200
  - **Browser profile:** with `launch.userDataDir` every page shares one
1089
- context, so `newContext()` is refused there.
1201
+ context, and with `launch.profile: 'per-test'` the test is one browser,
1202
+ so `newContext()` is refused in both.
1090
1203
  - **Debugging:** an agent that joins a paused run (`take_wheel`,
1091
1204
  `attach_debug_session`) lands in the context the run is on (Chromium).
1092
1205
  - **Names:** a function or variable of your own called `newContext` /
@@ -1189,8 +1302,10 @@ Pop one iframe scope. Idempotent at depth 0.
1189
1302
 
1190
1303
  The background of a Chrome MV3 extension is a service worker, and
1191
1304
  `chrome.*` lives there. Extensions load only into a browser started on a
1192
- profile — `launch.userDataDir` plus the `--load-extension` args in
1193
- `unotest.config`; without one every call below fails at once, naming it.
1305
+ profile — `launch.userDataDir` or `launch.profile: 'per-test'`, plus the
1306
+ `--load-extension` args in `unotest.config`; without one every call below
1307
+ fails at once, naming it. With `per-test` every test starts the worker
1308
+ from scratch, on empty storage.
1194
1309
  Headless needs `channel: "chromium"` (the headless shell ignores
1195
1310
  `--load-extension`).
1196
1311
 
@@ -1364,8 +1479,8 @@ scenario has not taken yet, waiting up to `{timeoutMs}` (default
1364
1479
  download is returned once.
1365
1480
 
1366
1481
  - `path` — absolute path of the saved copy. Read it with `waitForFile`,
1367
- which takes absolute paths as they are, or with `readFile` /
1368
- `readFileInfo`.
1482
+ `readFile` or `readFileInfo`: the downloads are one of the directories
1483
+ a scenario reads from.
1369
1484
  - `name` — the file name the site suggested.
1370
1485
  - `{name}` — substring or regex of the file name. A download that does
1371
1486
  not match stays for a later call.
@@ -1392,6 +1507,27 @@ returns the file content. Relative `path` resolves against
1392
1507
  `sandbox.shellCwd`. `options`: `{timeoutMs}` (default 20 000 ms). On
1393
1508
  expiry the step fails with the file path and the last observed state.
1394
1509
 
1510
+ **Where the file oracles read.** `waitForFile`, `assertNoFile`,
1511
+ `waitForJsonLine`, `readJsonLine`, `assertNoJsonLine`, `waitForFileCount`
1512
+ and `assertFileCount` read by the same rule as `readFile`: a path relative
1513
+ to `sandbox.shellCwd`, or an absolute one inside the project, the run's
1514
+ downloads or a directory listed in `sandbox.readRoots`. Anything else —
1515
+ `../` out of them, an absolute path elsewhere, a symlink that leads out —
1516
+ fails the step with a scenario error naming the path and the directories
1517
+ it left. The rule holds on every read of a wait: a file that appears as a
1518
+ symlink out is refused when it appears, and a file that does not exist
1519
+ yet is judged by the nearest directory above it that does.
1520
+
1521
+ A background process that writes outside the project (a listener's log
1522
+ in `/tmp`) needs its directory in the config:
1523
+
1524
+ ```js
1525
+ // unotest/unotest.config.mjs
1526
+ export default {
1527
+ sandbox: { readRoots: ['/tmp/my-listener'] },
1528
+ };
1529
+ ```
1530
+
1395
1531
  ```js
1396
1532
  step("Wait for the bot reply in the JSONL feed", () => {
1397
1533
  line = waitForFile('events.jsonl', /"type":"reply"/, {timeoutMs: 30000});
@@ -1503,6 +1639,25 @@ step("Send button uses the app's own label", () => {
1503
1639
  generate the JSON from them instead (see Scenarios, "App constants in
1504
1640
  scenarios").
1505
1641
 
1642
+ ### `env(name)` → `string | null`
1643
+
1644
+ An environment value the test can do without. It reads what a bare
1645
+ `UPPER_SNAKE` name reads — the environment the run started in, then
1646
+ `unotest/.env` — but returns `null` for a name that is not set instead
1647
+ of failing the step. Use it to decide:
1648
+
1649
+ ```js
1650
+ if (env('LIVE_API') == null) {
1651
+ note('live API', 'not configured, checks run against fixtures');
1652
+ } else {
1653
+ apiCall('GET', env('LIVE_API') + '/health');
1654
+ }
1655
+ ```
1656
+
1657
+ A secret — a name from `unotest/.secrets`, or one a box injects — is
1658
+ refused: `env()` returns a plain string, and a credential must stay
1659
+ masked. Write the secret's bare name where its value is typed instead.
1660
+
1506
1661
  ### `readFile(path, options?)` → `string`
1507
1662
 
1508
1663
  Read a whole file once and return its content: the text by default, or
@@ -1520,10 +1675,11 @@ step("Export is a PNG", () => {
1520
1675
  - `options.encoding`: `'utf8'` (default) or `'base64'`. Any other value
1521
1676
  or option key fails the step.
1522
1677
  - The path is relative to `sandbox.shellCwd` (the project root by
1523
- default), or absolute when it lies inside the project or is a file
1524
- `waitForDownload()` saved (`readFile(file.path)`). `../` out of the
1525
- project, an absolute path elsewhere, or a symlink that leads out is
1526
- refused.
1678
+ default), or absolute when it lies inside the project, is a file
1679
+ `waitForDownload()` saved (`readFile(file.path)`) or lies in a directory
1680
+ listed in `sandbox.readRoots`. `../` out of them, an absolute path
1681
+ elsewhere, or a symlink that leads out is refused — also for a file that
1682
+ does not exist yet, under a directory that leads out.
1527
1683
  - No poll: a missing file fails at once with `file not found: <path>`.
1528
1684
 
1529
1685
  ### `readFileInfo(path)` → `object`
@@ -2013,6 +2169,62 @@ screenshot('landing', {fullPage: true}); // full scroll height
2013
2169
  Keep explicit `screenshot()` calls for the moments a green run has to
2014
2170
  prove; the flag is for seeing the whole run afterwards.
2015
2171
 
2172
+ ### `consoleEntries(options?)` → `[{ level, text, url }]`
2173
+
2174
+ What the active page wrote to its console, oldest first. The page is
2175
+ recorded from the moment it opened, so the usual order works: act, then
2176
+ ask. An extension's own page (`chrome-extension://…/popup.html`) is a
2177
+ page like any other.
2178
+
2179
+ ```js
2180
+ click(getByRole('button', { name: 'Save' }));
2181
+ errors = consoleEntries({ level: 'error' });
2182
+ assertTrue(errors.length == 0, json(errors));
2183
+
2184
+ before = nowMs();
2185
+ click(getByRole('button', { name: 'Retry' }));
2186
+ assertTrue(consoleEntries({ level: ['warn', 'error'], since: before }).length == 1, 'one complaint');
2187
+ ```
2188
+
2189
+ - `level` of each entry: `log`, `info`, `warn`, `error` or `debug`
2190
+ (`console.table`, `console.dir` and the like read as `log`).
2191
+ - `url` — the page's address when the line was written.
2192
+ - `{level}` — one level or a list of them; an unknown level is refused.
2193
+ - `{since}` — epoch ms, as `nowMs()` gives: only lines from then on.
2194
+ - The last 500 lines of the context are kept, the oldest go first.
2195
+ - A service worker's console is not a page's: it is not here.
2196
+ - Independent of `UNOTEST_CONSOLE`, which decides only whether a run
2197
+ also writes `console.json` into its artifacts and failure bundles.
2198
+
2199
+ ### `pageErrors(options?)` → `[{ message, stack }]`
2200
+
2201
+ Errors the active page threw and nothing caught — an exception out of an
2202
+ event handler, a rejected promise nobody awaited. Oldest first; `stack`
2203
+ is empty when the browser gave none. The last 100 are kept.
2204
+
2205
+ ```js
2206
+ click(getByRole('button', { name: 'Load' }));
2207
+ thrown = pageErrors();
2208
+ assertTrue(thrown.length == 0, json(thrown));
2209
+ ```
2210
+
2211
+ - `{since}` — epoch ms, as `nowMs()` gives.
2212
+
2213
+ ### `assertNoConsoleErrors(options?)`
2214
+
2215
+ Fails when the active page logged console errors, or threw errors
2216
+ nothing caught, quoting the first three and counting the rest.
2217
+
2218
+ ```js
2219
+ goto('/checkout');
2220
+ assertNoConsoleErrors();
2221
+ assertNoConsoleErrors({ includePageErrors: false, since: before });
2222
+ ```
2223
+
2224
+ - `{includePageErrors}` — default `true`: an uncaught error is worse than
2225
+ a logged one. `false` looks at console errors only.
2226
+ - `{since}` — epoch ms, as `nowMs()` gives: only from then on.
2227
+
2016
2228
  ---
2017
2229
 
2018
2230
  ## Logging and notes
@@ -2238,14 +2450,38 @@ together) runs out are cut, and so is whatever a second Stop skips.
2238
2450
  Each such block is reported in the run result as a teardown failure —
2239
2451
  the stand may be left as the test set it.
2240
2452
 
2241
- **The run's own budget is a deadline.** `UNOTEST_MAX_DURATION_MS`
2242
- (default 5 min) ends the test the moment it runs out: an action still
2453
+ **The run's own budget is a deadline.** `testTimeoutMs` in config
2454
+ (default 5 min), `UNOTEST_MAX_DURATION_MS` for one run, or
2455
+ `setTestTimeout(ms)` inside the test ends the test the moment it runs out: an action still
2243
2456
  running is abandoned there, and one that finished past it fails the
2244
2457
  test on that line — the last action included. The abandoned browser
2245
2458
  call lives on until its own timeout or until the context closes; the
2246
2459
  teardown pass runs anyway, on `teardownTimeoutMs`, not on what the test
2247
2460
  left of its budget.
2248
2461
 
2462
+ ### `setTestTimeout(ms)`
2463
+
2464
+ This test's own time budget, counted from its start — Playwright's
2465
+ `test.setTimeout`. It replaces `testTimeoutMs` and
2466
+ `UNOTEST_MAX_DURATION_MS` for the test it runs in, longer or shorter:
2467
+
2468
+ ```js
2469
+ // unotest.config.mjs: testTimeoutMs: 30000 — every test gets 30 s
2470
+ function test_theme_rebuild() {
2471
+ setTestTimeout(90000); // this one rebuilds the theme, give it 90 s
2472
+ // …
2473
+ }
2474
+ ```
2475
+
2476
+ Running out is a failure, not a stop: the test is red — counted as
2477
+ errored (`‼`), like any run budget — with
2478
+ `setTestTimeout(90000) budget exhausted` (or `testTimeoutMs budget
2479
+ (30000ms) exhausted`), the action it cut is abandoned, and the teardown
2480
+ blocks play. Stop in the viewer aborts the run instead — same teardown,
2481
+ but the run reads as stopped, not failed. Call it in the `test_*` body:
2482
+ in a helper or a `flow_*` it is reported by
2483
+ `lint:set-test-timeout-outside-test`.
2484
+
2249
2485
  Rules (lint, `teardown-shape`): the label is a string literal; no
2250
2486
  `return` inside, no `break` out of it (a `break` in a loop inside is
2251
2487
  fine); no `teardown` inside a `teardown`. Allowed in tests, flows and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unotest/web",
3
- "version": "0.45.0",
3
+ "version": "0.46.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": {
@@ -77,6 +77,7 @@
77
77
  "dependencies": {
78
78
  "@faker-js/faker": "^10.5.0",
79
79
  "@modelcontextprotocol/sdk": "^1.0.4",
80
+ "ajv": "^8.20.0",
80
81
  "chokidar": "^4.0.3",
81
82
  "dotenv": "^16.4.5",
82
83
  "playwright": "1.62.1",
@@ -84,18 +85,18 @@
84
85
  "tsx": "^4.19.2",
85
86
  "yaml": "^2.6.0",
86
87
  "zod": "^3.23.8",
87
- "@unotest/core": "^0.45.0",
88
- "@unotest/dsl": "^0.45.0",
89
- "@unotest/protocol": "^0.45.0",
90
- "@unotest/viewer": "^0.45.0",
91
- "@unotest/grounder-client": "^0.45.0"
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"
92
93
  },
93
94
  "devDependencies": {
94
95
  "@types/node": "^22.10.0",
95
96
  "javascript-obfuscator": "^4.1.1",
96
97
  "tsup": "^8.5.1",
97
98
  "typescript": "^5.7.2",
98
- "@unotest/grounder": "^0.45.0",
99
- "@unotest/judge": "^0.45.0"
99
+ "@unotest/judge": "^0.46.0",
100
+ "@unotest/grounder": "^0.46.0"
100
101
  }
101
102
  }
@@ -92,7 +92,7 @@ interface Locator {
92
92
  }
93
93
 
94
94
  /**
95
- * click(locator, { force?, timeout?, noWaitAfter? })
95
+ * click(locator, { force?, timeout?, position?, modifiers?, button?, clickCount? })
96
96
  *
97
97
  * Click an element.
98
98
  */
@@ -134,7 +134,7 @@ declare function hover(locator: Locator, options?: any): void;
134
134
  declare function scrollIntoView(locator: Locator, options?: any): void;
135
135
 
136
136
  /**
137
- * fill(locator, value, { timeout?, noWaitAfter? })
137
+ * fill(locator, value, { force?, timeout? })
138
138
  *
139
139
  * Clear and type a value into an input.
140
140
  */
@@ -176,11 +176,25 @@ declare function uploadFile(input: Locator, files: any): void;
176
176
  declare function clipboardPaste(target: Locator, text: string): void;
177
177
 
178
178
  /**
179
- * assertText(locator, expected, { exact?, timeout? })
179
+ * assertText(locator, expected, { exact?, not?, timeout? })
180
180
  *
181
- * Element's text equals/contains `expected`.
181
+ * Element's text equals `expected` (a string; exact: false — contains it) or matches it (a /regex/). {not: true} waits until it no longer does. Empty: assertText(loc, ''); not empty: assertText(loc, '', {not: true}).
182
182
  */
183
- declare function assertText(locator: Locator, text: string, options?: any): void;
183
+ declare function assertText(locator: Locator, text: string | RegExp, options?: any): void;
184
+
185
+ /**
186
+ * assertTexts(locator, expected, { exact?, timeout? })
187
+ *
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
+ */
190
+ declare function assertTexts(locator: Locator, texts: unknown[], options?: any): void;
191
+
192
+ /**
193
+ * assertAttribute(locator, name, expected, { exact?, not?, timeout? })
194
+ *
195
+ * Polls the attribute `name` until it equals `expected` (exact: false — contains) or matches a /regex/. An absent attribute matches nothing. {not: true} waits until it no longer matches or is gone.
196
+ */
197
+ declare function assertAttribute(locator: Locator, name: string, value: string | RegExp, options?: any): void;
184
198
 
185
199
  /**
186
200
  * assertVisible(locator, { timeout? })
@@ -252,6 +266,27 @@ declare function assertEqual(actual: any, expected: any): void;
252
266
  */
253
267
  declare function assertTrue(condition: boolean, message?: string): void;
254
268
 
269
+ /**
270
+ * assertSchema(value, schema)
271
+ *
272
+ * Check a value against a JSON Schema (draft 2020-12), e.g. one your app generates from its zod schemas: assertSchema(res.body, readJson('unotest/fixtures/message.schema.json')). FAILS naming every broken field by path (`/items/0/id`) with what is wrong. External $ref is refused; `format` is not checked.
273
+ */
274
+ declare function assertSchema(value: any, schema: any): void;
275
+
276
+ /**
277
+ * skip(reason, condition?)
278
+ *
279
+ * End the test as skipped when the condition is true (always, without one) — it does not apply here: skip('no live API configured', env('LIVE_API') == null). The run reports skipped with the reason, not passed and not failed; teardown blocks already registered still play. A test guard: at the level of the test_* body, next to its steps, not inside one.
280
+ */
281
+ declare function skip(reason: string, condition?: boolean): void;
282
+
283
+ /**
284
+ * setTestTimeout(ms)
285
+ *
286
+ * This test's own time budget, counted from its start (Playwright's test.setTimeout). Replaces config testTimeoutMs and UNOTEST_MAX_DURATION_MS for this test. A deadline: an action still running when it runs out is cut, the test fails naming the budget, teardown blocks still play. Only in a test_* body.
287
+ */
288
+ declare function setTestTimeout(ms: number): void;
289
+
255
290
  /**
256
291
  * filter(locator, { hasText?, hasNotText?, has?, hasNot? })
257
292
  *
@@ -301,6 +336,27 @@ declare function screenshot(name?: string, options?: any): string;
301
336
  */
302
337
  declare function note(label: string, value: any): void;
303
338
 
339
+ /**
340
+ * consoleEntries({ level?, since? }) → [{ level, text, url }]
341
+ *
342
+ * What the active page wrote to its console, oldest first: `level` is log, info, warn, error or debug. Recorded from the moment the page opened, so act first, then ask. Extension pages (chrome-extension://) too. options: {level} (one level or a list), {since} (epoch ms, from nowMs()).
343
+ */
344
+ declare function consoleEntries(options?: any): any;
345
+
346
+ /**
347
+ * pageErrors({ since? }) → [{ message, stack }]
348
+ *
349
+ * Errors the active page threw and nothing caught (an uncaught exception, a rejected promise), oldest first; `stack` is empty when the browser gave none. options: {since} (epoch ms, from nowMs()).
350
+ */
351
+ declare function pageErrors(options?: any): any;
352
+
353
+ /**
354
+ * assertNoConsoleErrors({ includePageErrors?, since? })
355
+ *
356
+ * FAIL when the active page logged console errors, or threw uncaught ones (unless {includePageErrors: false}), quoting the first three. With `since` (epoch ms, from nowMs()) only from then on.
357
+ */
358
+ declare function assertNoConsoleErrors(options?: any): void;
359
+
304
360
  /**
305
361
  * evaluate(js, arg?) → any
306
362
  *
@@ -574,6 +630,13 @@ declare function parseJson(text: string): any;
574
630
  */
575
631
  declare function urlParam(url: string, key: string): any;
576
632
 
633
+ /**
634
+ * env(name) → string | null
635
+ *
636
+ * 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.
637
+ */
638
+ declare function env(name: string): any;
639
+
577
640
  /**
578
641
  * shell(cmd, ...args, options?) → { stdout, stderr, code }
579
642
  *