@unotest/web 0.44.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 +73 -3
- package/README.md +2 -0
- package/dist/config/schema.d.ts +385 -46
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +12 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +37 -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-DEmGI704.d.ts → interfaces-CYoL4Taq.d.ts} +189 -6
- 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 +653 -5
- package/package.json +9 -8
- package/types/unotest-dsl.d.ts +218 -8
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/
|
|
88
|
-
"@unotest/
|
|
89
|
-
"@unotest/
|
|
90
|
-
"@unotest/
|
|
91
|
-
"@unotest/
|
|
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/judge": "^0.
|
|
99
|
-
"@unotest/grounder": "^0.
|
|
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? })
|
|
@@ -238,6 +252,13 @@ declare function assertJudge(locator: Locator, rubric: string): void;
|
|
|
238
252
|
*/
|
|
239
253
|
declare function assertScreenshot(nameOrLocator: any, nameOrOptions?: any, options?: any): void;
|
|
240
254
|
|
|
255
|
+
/**
|
|
256
|
+
* assertEqual(actual, expected)
|
|
257
|
+
*
|
|
258
|
+
* Deep equality as JSON sees it: key order does not count, a missing key differs from a null one. FAILS naming the first differing path (`chat.labels[0].send`) with both values, plus a line diff of the two sides. The DSL form of toEqual.
|
|
259
|
+
*/
|
|
260
|
+
declare function assertEqual(actual: any, expected: any): void;
|
|
261
|
+
|
|
241
262
|
/**
|
|
242
263
|
* assertTrue(condition, message?)
|
|
243
264
|
*
|
|
@@ -245,6 +266,27 @@ declare function assertScreenshot(nameOrLocator: any, nameOrOptions?: any, optio
|
|
|
245
266
|
*/
|
|
246
267
|
declare function assertTrue(condition: boolean, message?: string): void;
|
|
247
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
|
+
|
|
248
290
|
/**
|
|
249
291
|
* filter(locator, { hasText?, hasNotText?, has?, hasNot? })
|
|
250
292
|
*
|
|
@@ -294,6 +336,27 @@ declare function screenshot(name?: string, options?: any): string;
|
|
|
294
336
|
*/
|
|
295
337
|
declare function note(label: string, value: any): void;
|
|
296
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
|
+
|
|
297
360
|
/**
|
|
298
361
|
* evaluate(js, arg?) → any
|
|
299
362
|
*
|
|
@@ -302,11 +365,46 @@ declare function note(label: string, value: any): void;
|
|
|
302
365
|
declare function evaluate(jsBody: string, ...args: unknown[]): any;
|
|
303
366
|
|
|
304
367
|
/**
|
|
305
|
-
*
|
|
368
|
+
* evaluateOnLocator(locator, js, ...args) → any
|
|
369
|
+
*
|
|
370
|
+
* Like evaluate, on the matched element: the body is called with the element, then the extra arguments by the same rule (`(el, x) => …`, two or more → `(el, [a, b]) => …`). Last resort — prefer typed getters.
|
|
371
|
+
*/
|
|
372
|
+
declare function evaluateOnLocator(locator: Locator, jsBody: string, ...args: unknown[]): any;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* setPage(index, options?)
|
|
376
|
+
*
|
|
377
|
+
* Switch the active tab/page by index (0-based) and bring it to front. {activate: false} switches only the tab the steps act on and leaves the visible tab as is.
|
|
378
|
+
*/
|
|
379
|
+
declare function setPage(indexOrTitle: string | number, options?: any): void;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* waitForPage(options?) → number
|
|
383
|
+
*
|
|
384
|
+
* Wait for a tab the scenario has not been handed yet (window.open, target=_blank, an extension), switch to it and return its index. Counts a tab that opened before the call. {timeoutMs} defaults to the navigation timeout.
|
|
385
|
+
*/
|
|
386
|
+
declare function waitForPage(options?: any): number;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* bringToFront(index?)
|
|
390
|
+
*
|
|
391
|
+
* Bring a tab to front — the visible one — without changing the tab the steps act on. No index: the active tab.
|
|
392
|
+
*/
|
|
393
|
+
declare function bringToFront(index?: number): void;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* newPage(url?) → number
|
|
306
397
|
*
|
|
307
|
-
*
|
|
398
|
+
* Open a new tab in the active browser context, make it active and return its index. With a url the tab opens it (relative to the base URL, as goto).
|
|
308
399
|
*/
|
|
309
|
-
declare function
|
|
400
|
+
declare function newPage(url?: string): number;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* closePage(index?)
|
|
404
|
+
*
|
|
405
|
+
* Close a tab; no index: the active one. Closing the active tab makes the tab that was active before it active again. The only open tab cannot be closed.
|
|
406
|
+
*/
|
|
407
|
+
declare function closePage(index?: number): void;
|
|
310
408
|
|
|
311
409
|
/**
|
|
312
410
|
* setViewport({width, height})
|
|
@@ -511,6 +609,34 @@ declare function today(): string;
|
|
|
511
609
|
*/
|
|
512
610
|
declare function daysFromNow(days: number): string;
|
|
513
611
|
|
|
612
|
+
/**
|
|
613
|
+
* textMatches(text, /re/) → false | string[]
|
|
614
|
+
*
|
|
615
|
+
* First match of a regex literal: false on a miss, else `[whole, group1, …]` (a group that took no part is null). The array is truthy, so it also reads as a test: `m = textMatches(url, /room=(\d+)/); id = m[1];`. For a plain substring use textContains.
|
|
616
|
+
*/
|
|
617
|
+
declare function textMatches(text: string, pattern: any): any;
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* parseJson(text) → any
|
|
621
|
+
*
|
|
622
|
+
* Parse a JSON string (an api body, a data attribute, a request's postData) into a value you walk with `.` and `[]`. FAILS on invalid JSON, quoting the input; an empty string is not JSON.
|
|
623
|
+
*/
|
|
624
|
+
declare function parseJson(text: string): any;
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* urlParam(url, key) → string | null
|
|
628
|
+
*
|
|
629
|
+
* A query parameter of a URL, decoded; null when the key is absent, '' when it is empty. A path without a host (`/room?id=7`) works too. The DSL form of `new URL(u).searchParams.get(key)`.
|
|
630
|
+
*/
|
|
631
|
+
declare function urlParam(url: string, key: string): any;
|
|
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
|
+
|
|
514
640
|
/**
|
|
515
641
|
* shell(cmd, ...args, options?) → { stdout, stderr, code }
|
|
516
642
|
*
|
|
@@ -553,6 +679,27 @@ declare function assertNoJsonLine(path: string, filter: any, options?: any): voi
|
|
|
553
679
|
*/
|
|
554
680
|
declare function readJsonLine(path: string, filter: any): any;
|
|
555
681
|
|
|
682
|
+
/**
|
|
683
|
+
* readJson(path) → any
|
|
684
|
+
*
|
|
685
|
+
* A whole JSON file, read once and parsed: app constants a build script exported, fixture objects. Path is relative to config `shellCwd` and must stay inside it. FAILS at once when the path leaves the project, the file is missing or it is not valid JSON. Walk the result with `.` and `[]`.
|
|
686
|
+
*/
|
|
687
|
+
declare function readJson(path: string): any;
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* readFile(path, options?) → string
|
|
691
|
+
*
|
|
692
|
+
* A whole file, read once: its text, or its bytes as base64 with {encoding: 'base64'}. Path is relative to config `shellCwd`, or absolute inside the project or to a file waitForDownload saved (its .path). FAILS at once when the path or a symlink leads out, the file is missing or the encoding is unknown.
|
|
693
|
+
*/
|
|
694
|
+
declare function readFile(path: string, options?: any): string;
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* readFileInfo(path) → object
|
|
698
|
+
*
|
|
699
|
+
* What to assert about a file without reading it: {size} in bytes, plus png: {width, height} for a PNG (from its header). Same path rules and failures as readFile.
|
|
700
|
+
*/
|
|
701
|
+
declare function readFileInfo(path: string): any;
|
|
702
|
+
|
|
556
703
|
/**
|
|
557
704
|
* waitForFileCount(path, pattern, count, options?) → number
|
|
558
705
|
*
|
|
@@ -679,6 +826,13 @@ declare function getInnerText(locator: Locator): string;
|
|
|
679
826
|
*/
|
|
680
827
|
declare function getInputValue(locator: Locator): string;
|
|
681
828
|
|
|
829
|
+
/**
|
|
830
|
+
* boundingBox(locator) → {x, y, width, height} | null
|
|
831
|
+
*
|
|
832
|
+
* Where the element lies in the viewport, CSS pixels; null when it is not rendered (display:none, detached). Does not scroll.
|
|
833
|
+
*/
|
|
834
|
+
declare function boundingBox(locator: Locator): any;
|
|
835
|
+
|
|
682
836
|
/**
|
|
683
837
|
* getTitle() → string
|
|
684
838
|
*
|
|
@@ -756,6 +910,13 @@ declare function waitForCount(locator: Locator, count: number, options?: any): v
|
|
|
756
910
|
*/
|
|
757
911
|
declare function pause(ms: number): void;
|
|
758
912
|
|
|
913
|
+
/**
|
|
914
|
+
* waitForDownload(options?) → { path, name }
|
|
915
|
+
*
|
|
916
|
+
* The first file the page downloaded that the scenario has not taken yet, saved with the run: `path` is absolute (read it with waitForFile), `name` is the name the site suggested. Downloads are recorded from the start, so click first, then call this. options: {timeoutMs} (default: the navigation timeout), {name} (substring or regex of the file name).
|
|
917
|
+
*/
|
|
918
|
+
declare function waitForDownload(options?: any): any;
|
|
919
|
+
|
|
759
920
|
/**
|
|
760
921
|
* clockFreeze(iso)
|
|
761
922
|
*
|
|
@@ -777,6 +938,55 @@ declare function clockTick(ms: number): void;
|
|
|
777
938
|
*/
|
|
778
939
|
declare function clockRun(): void;
|
|
779
940
|
|
|
941
|
+
/**
|
|
942
|
+
* mockRoute(glob, response)
|
|
943
|
+
*
|
|
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.
|
|
945
|
+
*/
|
|
946
|
+
declare function mockRoute(glob: string, response: any): void;
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* unmockRoute(glob?)
|
|
950
|
+
*
|
|
951
|
+
* Take off the mock at `glob`, or every mock when called without one.
|
|
952
|
+
*/
|
|
953
|
+
declare function unmockRoute(glob?: string): void;
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* waitForRequest(glob, { timeoutMs? }) → { url, method, headers, postData, postDataJson }
|
|
957
|
+
*
|
|
958
|
+
* Wait for a request matching `glob` and return it; `postDataJson` is the body read as JSON (null when it is not). On a mocked mask it also finds a request sent before the call, each one returned once; on any other mask only requests from the call on. Default timeout 20 s.
|
|
959
|
+
*/
|
|
960
|
+
declare function waitForRequest(glob: string, options?: any): any;
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* assertNoRequest(glob, { since? })
|
|
964
|
+
*
|
|
965
|
+
* FAIL if the recording abort at `glob` — mockRoute(glob, {abort: true, record: true}) — stopped any request; with `since` (epoch ms, from nowMs()) only from then on. A mask without a recording abort is an error, not a pass.
|
|
966
|
+
*/
|
|
967
|
+
declare function assertNoRequest(glob: string, options?: any): void;
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* waitForServiceWorker({ timeoutMs?, url? }) → string
|
|
971
|
+
*
|
|
972
|
+
* Wait for a browser extension's service worker and return its URL (`chrome-extension://<id>/sw.js`); a running one comes back at once. `url` — substring or /regex/ when several extensions run. Extensions load only with `launch.userDataDir`: without it the call fails at once.
|
|
973
|
+
*/
|
|
974
|
+
declare function waitForServiceWorker(options?: any): string;
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* extensionId() → string
|
|
978
|
+
*
|
|
979
|
+
* The id of the extension whose service worker runs — for `goto('chrome-extension://' + extensionId() + '/popup.html')`.
|
|
980
|
+
*/
|
|
981
|
+
declare function extensionId(): string;
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* evaluateInServiceWorker(js, ...args) → any
|
|
985
|
+
*
|
|
986
|
+
* Like evaluate, in the extension's service worker — the way to `chrome.storage`, `chrome.tabs`, `chrome.runtime`. Same argument and JSON-return rules; waits for the worker if none runs. Each call finds the worker anew: MV3 stops an idle one.
|
|
987
|
+
*/
|
|
988
|
+
declare function evaluateInServiceWorker(jsBody: string, ...args: unknown[]): any;
|
|
989
|
+
|
|
780
990
|
/** step(label, [options,] body)
|
|
781
991
|
*
|
|
782
992
|
* Groups statements under a labelled block. Every direct child of a
|