@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unotest/web",
3
- "version": "0.44.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/dsl": "^0.44.0",
88
- "@unotest/core": "^0.44.0",
89
- "@unotest/viewer": "^0.44.0",
90
- "@unotest/grounder-client": "^0.44.0",
91
- "@unotest/protocol": "^0.44.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/judge": "^0.44.0",
99
- "@unotest/grounder": "^0.44.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? })
@@ -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
- * setPage(index)
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
- * Switch the active tab/page by index (0-based).
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 setPage(indexOrTitle: string | number): void;
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