@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.
- package/CHANGELOG.md +37 -3
- package/README.md +2 -0
- package/dist/config/schema.d.ts +205 -18
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +5 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +19 -2
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +1 -1
- package/dist/{interfaces-OvnJKELk.d.ts → interfaces-CYoL4Taq.d.ts} +42 -2
- 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 +248 -12
- package/package.json +9 -8
- package/types/unotest-dsl.d.ts +68 -5
package/guides/dsl-reference.md
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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`
|
|
1193
|
-
`unotest.config`; without one every call below
|
|
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
|
-
|
|
1368
|
-
|
|
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
|
|
1524
|
-
`waitForDownload()` saved (`readFile(file.path)`)
|
|
1525
|
-
|
|
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.** `
|
|
2242
|
-
(default 5 min)
|
|
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.
|
|
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.
|
|
88
|
-
"@unotest/dsl": "^0.
|
|
89
|
-
"@unotest/protocol": "^0.
|
|
90
|
-
"@unotest/viewer": "^0.
|
|
91
|
-
"@unotest/grounder-client": "^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/
|
|
99
|
-
"@unotest/
|
|
99
|
+
"@unotest/judge": "^0.46.0",
|
|
100
|
+
"@unotest/grounder": "^0.46.0"
|
|
100
101
|
}
|
|
101
102
|
}
|
package/types/unotest-dsl.d.ts
CHANGED
|
@@ -92,7 +92,7 @@ interface Locator {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* click(locator, { force?, timeout?,
|
|
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, {
|
|
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
|
|
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
|
*
|