@unotest/web 0.43.2 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/write-e2e-test/SKILL.md +7 -6
- package/.claude/skills/write-e2e-test-ground/SKILL.md +7 -6
- package/CHANGELOG.md +127 -3
- package/dist/config/schema.d.ts +260 -62
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +12 -3
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +241 -6
- 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-ClW2IRgY.d.ts → interfaces-OvnJKELk.d.ts} +232 -9
- 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/agent-integration.md +8 -0
- package/guides/dsl-reference.md +846 -10
- package/package.json +9 -8
- package/types/unotest-dsl.d.ts +199 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unotest/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.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": {
|
|
@@ -80,21 +80,22 @@
|
|
|
80
80
|
"chokidar": "^4.0.3",
|
|
81
81
|
"dotenv": "^16.4.5",
|
|
82
82
|
"playwright": "1.62.1",
|
|
83
|
+
"pngjs": "7.0.0",
|
|
83
84
|
"tsx": "^4.19.2",
|
|
84
85
|
"yaml": "^2.6.0",
|
|
85
86
|
"zod": "^3.23.8",
|
|
86
|
-
"@unotest/core": "^0.
|
|
87
|
-
"@unotest/dsl": "^0.
|
|
88
|
-
"@unotest/
|
|
89
|
-
"@unotest/
|
|
90
|
-
"@unotest/
|
|
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"
|
|
91
92
|
},
|
|
92
93
|
"devDependencies": {
|
|
93
94
|
"@types/node": "^22.10.0",
|
|
94
95
|
"javascript-obfuscator": "^4.1.1",
|
|
95
96
|
"tsup": "^8.5.1",
|
|
96
97
|
"typescript": "^5.7.2",
|
|
97
|
-
"@unotest/
|
|
98
|
-
"@unotest/
|
|
98
|
+
"@unotest/grounder": "^0.45.0",
|
|
99
|
+
"@unotest/judge": "^0.45.0"
|
|
99
100
|
}
|
|
100
101
|
}
|
package/types/unotest-dsl.d.ts
CHANGED
|
@@ -231,6 +231,20 @@ declare function assertUrl(pattern: string): void;
|
|
|
231
231
|
*/
|
|
232
232
|
declare function assertJudge(locator: Locator, rubric: string): void;
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* assertScreenshot(name | locator, name?, { maxDiffRatio?, threshold?, mask?, ignore?, fullPage? })
|
|
236
|
+
*
|
|
237
|
+
* The page (or an element) against the PNG baseline unotest/e2e/<scenario>.screenshots/<name>.png. A pixel differs when any channel differs by more than threshold (default 32); fails when the share of differing pixels is MORE than maxDiffRatio (default 0). mask: locators, ignore: rectangles in baseline pixels — both left out of the comparison. No baseline: fails and saves the snapshot as a candidate; accept with e2e --update-screenshots. On failure the run keeps expected / actual / diff.
|
|
238
|
+
*/
|
|
239
|
+
declare function assertScreenshot(nameOrLocator: any, nameOrOptions?: any, options?: any): void;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* assertEqual(actual, expected)
|
|
243
|
+
*
|
|
244
|
+
* 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.
|
|
245
|
+
*/
|
|
246
|
+
declare function assertEqual(actual: any, expected: any): void;
|
|
247
|
+
|
|
234
248
|
/**
|
|
235
249
|
* assertTrue(condition, message?)
|
|
236
250
|
*
|
|
@@ -295,11 +309,67 @@ declare function note(label: string, value: any): void;
|
|
|
295
309
|
declare function evaluate(jsBody: string, ...args: unknown[]): any;
|
|
296
310
|
|
|
297
311
|
/**
|
|
298
|
-
*
|
|
312
|
+
* evaluateOnLocator(locator, js, ...args) → any
|
|
313
|
+
*
|
|
314
|
+
* 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.
|
|
315
|
+
*/
|
|
316
|
+
declare function evaluateOnLocator(locator: Locator, jsBody: string, ...args: unknown[]): any;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* setPage(index, options?)
|
|
320
|
+
*
|
|
321
|
+
* 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.
|
|
322
|
+
*/
|
|
323
|
+
declare function setPage(indexOrTitle: string | number, options?: any): void;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* waitForPage(options?) → number
|
|
327
|
+
*
|
|
328
|
+
* 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.
|
|
329
|
+
*/
|
|
330
|
+
declare function waitForPage(options?: any): number;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* bringToFront(index?)
|
|
334
|
+
*
|
|
335
|
+
* Bring a tab to front — the visible one — without changing the tab the steps act on. No index: the active tab.
|
|
336
|
+
*/
|
|
337
|
+
declare function bringToFront(index?: number): void;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* newPage(url?) → number
|
|
341
|
+
*
|
|
342
|
+
* 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).
|
|
343
|
+
*/
|
|
344
|
+
declare function newPage(url?: string): number;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* closePage(index?)
|
|
299
348
|
*
|
|
300
|
-
*
|
|
349
|
+
* 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.
|
|
301
350
|
*/
|
|
302
|
-
declare function
|
|
351
|
+
declare function closePage(index?: number): void;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* setViewport({width, height})
|
|
355
|
+
*
|
|
356
|
+
* Resize every tab of the browser, and the ones it opens later, without reloading. The scale (deviceScaleFactor) is set in the config, not here.
|
|
357
|
+
*/
|
|
358
|
+
declare function setViewport(size: any): void;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* newContext() → number
|
|
362
|
+
*
|
|
363
|
+
* Open another browser context — a separate client with its own cookies, storage and tabs — and make it active. Returns its number (the run's own is 0).
|
|
364
|
+
*/
|
|
365
|
+
declare function newContext(): number;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* setContext(index)
|
|
369
|
+
*
|
|
370
|
+
* Make browser context `index` active. Each keeps its own tab and frame, so switching back lands where it was.
|
|
371
|
+
*/
|
|
372
|
+
declare function setContext(index: number): void;
|
|
303
373
|
|
|
304
374
|
/**
|
|
305
375
|
* enterFrame(locator)
|
|
@@ -483,6 +553,27 @@ declare function today(): string;
|
|
|
483
553
|
*/
|
|
484
554
|
declare function daysFromNow(days: number): string;
|
|
485
555
|
|
|
556
|
+
/**
|
|
557
|
+
* textMatches(text, /re/) → false | string[]
|
|
558
|
+
*
|
|
559
|
+
* 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.
|
|
560
|
+
*/
|
|
561
|
+
declare function textMatches(text: string, pattern: any): any;
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* parseJson(text) → any
|
|
565
|
+
*
|
|
566
|
+
* 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.
|
|
567
|
+
*/
|
|
568
|
+
declare function parseJson(text: string): any;
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* urlParam(url, key) → string | null
|
|
572
|
+
*
|
|
573
|
+
* 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)`.
|
|
574
|
+
*/
|
|
575
|
+
declare function urlParam(url: string, key: string): any;
|
|
576
|
+
|
|
486
577
|
/**
|
|
487
578
|
* shell(cmd, ...args, options?) → { stdout, stderr, code }
|
|
488
579
|
*
|
|
@@ -525,6 +616,27 @@ declare function assertNoJsonLine(path: string, filter: any, options?: any): voi
|
|
|
525
616
|
*/
|
|
526
617
|
declare function readJsonLine(path: string, filter: any): any;
|
|
527
618
|
|
|
619
|
+
/**
|
|
620
|
+
* readJson(path) → any
|
|
621
|
+
*
|
|
622
|
+
* 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 `[]`.
|
|
623
|
+
*/
|
|
624
|
+
declare function readJson(path: string): any;
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* readFile(path, options?) → string
|
|
628
|
+
*
|
|
629
|
+
* 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.
|
|
630
|
+
*/
|
|
631
|
+
declare function readFile(path: string, options?: any): string;
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* readFileInfo(path) → object
|
|
635
|
+
*
|
|
636
|
+
* 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.
|
|
637
|
+
*/
|
|
638
|
+
declare function readFileInfo(path: string): any;
|
|
639
|
+
|
|
528
640
|
/**
|
|
529
641
|
* waitForFileCount(path, pattern, count, options?) → number
|
|
530
642
|
*
|
|
@@ -651,6 +763,13 @@ declare function getInnerText(locator: Locator): string;
|
|
|
651
763
|
*/
|
|
652
764
|
declare function getInputValue(locator: Locator): string;
|
|
653
765
|
|
|
766
|
+
/**
|
|
767
|
+
* boundingBox(locator) → {x, y, width, height} | null
|
|
768
|
+
*
|
|
769
|
+
* Where the element lies in the viewport, CSS pixels; null when it is not rendered (display:none, detached). Does not scroll.
|
|
770
|
+
*/
|
|
771
|
+
declare function boundingBox(locator: Locator): any;
|
|
772
|
+
|
|
654
773
|
/**
|
|
655
774
|
* getTitle() → string
|
|
656
775
|
*
|
|
@@ -728,6 +847,83 @@ declare function waitForCount(locator: Locator, count: number, options?: any): v
|
|
|
728
847
|
*/
|
|
729
848
|
declare function pause(ms: number): void;
|
|
730
849
|
|
|
850
|
+
/**
|
|
851
|
+
* waitForDownload(options?) → { path, name }
|
|
852
|
+
*
|
|
853
|
+
* 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).
|
|
854
|
+
*/
|
|
855
|
+
declare function waitForDownload(options?: any): any;
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* clockFreeze(iso)
|
|
859
|
+
*
|
|
860
|
+
* Stop time on the page at an ISO instant: `Date` reads it and no timer fires until clockTick / clockRun. Any instant, past or future.
|
|
861
|
+
*/
|
|
862
|
+
declare function clockFreeze(iso: string): void;
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* clockTick(ms)
|
|
866
|
+
*
|
|
867
|
+
* Move frozen time forward by ms, firing every timer due in it.
|
|
868
|
+
*/
|
|
869
|
+
declare function clockTick(ms: number): void;
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* clockRun()
|
|
873
|
+
*
|
|
874
|
+
* Let time on the page run again from where it stands.
|
|
875
|
+
*/
|
|
876
|
+
declare function clockRun(): void;
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* mockRoute(glob, response)
|
|
880
|
+
*
|
|
881
|
+
* 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.
|
|
882
|
+
*/
|
|
883
|
+
declare function mockRoute(glob: string, response: any): void;
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* unmockRoute(glob?)
|
|
887
|
+
*
|
|
888
|
+
* Take off the mock at `glob`, or every mock when called without one.
|
|
889
|
+
*/
|
|
890
|
+
declare function unmockRoute(glob?: string): void;
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* waitForRequest(glob, { timeoutMs? }) → { url, method, headers, postData, postDataJson }
|
|
894
|
+
*
|
|
895
|
+
* 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.
|
|
896
|
+
*/
|
|
897
|
+
declare function waitForRequest(glob: string, options?: any): any;
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* assertNoRequest(glob, { since? })
|
|
901
|
+
*
|
|
902
|
+
* 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.
|
|
903
|
+
*/
|
|
904
|
+
declare function assertNoRequest(glob: string, options?: any): void;
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* waitForServiceWorker({ timeoutMs?, url? }) → string
|
|
908
|
+
*
|
|
909
|
+
* 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.
|
|
910
|
+
*/
|
|
911
|
+
declare function waitForServiceWorker(options?: any): string;
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* extensionId() → string
|
|
915
|
+
*
|
|
916
|
+
* The id of the extension whose service worker runs — for `goto('chrome-extension://' + extensionId() + '/popup.html')`.
|
|
917
|
+
*/
|
|
918
|
+
declare function extensionId(): string;
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* evaluateInServiceWorker(js, ...args) → any
|
|
922
|
+
*
|
|
923
|
+
* 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.
|
|
924
|
+
*/
|
|
925
|
+
declare function evaluateInServiceWorker(jsBody: string, ...args: unknown[]): any;
|
|
926
|
+
|
|
731
927
|
/** step(label, [options,] body)
|
|
732
928
|
*
|
|
733
929
|
* Groups statements under a labelled block. Every direct child of a
|