@unotest/web 0.43.1 → 0.44.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 +165 -3
- package/dist/config/schema.d.ts +93 -31
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +5 -3
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +229 -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 +23 -6
- package/dist/inspection/page-inject.js +57 -26
- package/dist/{interfaces-7nMNCr2h.d.ts → interfaces-DEmGI704.d.ts} +108 -10
- 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 +30 -5
- package/guides/dsl-reference.md +433 -9
- package/package.json +9 -8
- package/types/unotest-dsl.d.ts +49 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InjectedDomHelpers } from '@unotest/grounder-client/dom-walker';
|
|
2
2
|
import { PickerMode } from '@unotest/protocol';
|
|
3
|
-
import { E as ElementHint } from '../interfaces-
|
|
3
|
+
import { E as ElementHint } from '../interfaces-DEmGI704.js';
|
|
4
4
|
import '../config/schema.js';
|
|
5
5
|
import 'zod';
|
|
6
6
|
|
|
@@ -147,6 +147,8 @@ interface SemanticDomResult {
|
|
|
147
147
|
|
|
148
148
|
interface BrowserSemanticDomOptions {
|
|
149
149
|
disambiguate?: boolean;
|
|
150
|
+
/** `TEXT_HIDDEN_TAGS` (grounder-client text-rules.ts), passed in. */
|
|
151
|
+
hiddenTextTags?: readonly string[];
|
|
150
152
|
}
|
|
151
153
|
declare function semanticDomAlgorithm(options?: BrowserSemanticDomOptions): SemanticDomResult;
|
|
152
154
|
|
|
@@ -351,7 +353,18 @@ type PickerOp = {
|
|
|
351
353
|
};
|
|
352
354
|
declare function pickerOverlayAlgorithm(op: PickerOp): DrainResult | void;
|
|
353
355
|
|
|
354
|
-
|
|
356
|
+
/** How much of the page a capture keeps. `redacted` (every capture unless a
|
|
357
|
+
* client asks otherwise): a script's content and a hidden field's value
|
|
358
|
+
* become a label with their length — an app's bootstrap data and a
|
|
359
|
+
* request-forgery token are not what a failure report needs (#265).
|
|
360
|
+
* `full` keeps both. A secret field's value is hidden in both. */
|
|
361
|
+
interface HtmlCaptureOptions {
|
|
362
|
+
full: boolean;
|
|
363
|
+
}
|
|
364
|
+
/** The whole document, or — `root` given — that one element, as the same
|
|
365
|
+
* redacted HTML. Called with an element by `Locator.evaluate`, which is why
|
|
366
|
+
* the element comes first. */
|
|
367
|
+
declare function serializeRuntimeDom(root: Element | null, opts: HtmlCaptureOptions): string;
|
|
355
368
|
|
|
356
369
|
interface FindElementMatch {
|
|
357
370
|
/** Shared with outline / aria-snapshot via `data-unotest-ref`.
|
|
@@ -418,10 +431,14 @@ declare function readDisabledState(el: Element): boolean;
|
|
|
418
431
|
declare function writeLocalStorageItem([key, value]: [string, string]): void;
|
|
419
432
|
/** Read a localStorage item (`null` when absent). */
|
|
420
433
|
declare function readLocalStorageItem(key: string): string | null;
|
|
421
|
-
/** Live outerHTML of an element. */
|
|
422
|
-
declare function readOuterHtml(el: Element): string;
|
|
423
434
|
/** Read every identifying attribute off a live element (role inferred from
|
|
424
435
|
* tag + ARIA, accname approximated). Returns a structured raw hint. */
|
|
425
|
-
declare function inspectElementRaw(el: Element
|
|
436
|
+
declare function inspectElementRaw(el: Element, args: {
|
|
437
|
+
hiddenTextTags: readonly string[];
|
|
438
|
+
readable?: {
|
|
439
|
+
whole: true;
|
|
440
|
+
limit: number;
|
|
441
|
+
};
|
|
442
|
+
}): ElementHintRaw;
|
|
426
443
|
|
|
427
|
-
export { type ElementHintRaw, ariaSnapshotAlgorithm, findElementAlgorithm, inspectElementRaw, outlineAlgorithm, pasteValue, pickerOverlayAlgorithm, readCheckedState, readDisabledState, readLocalStorageItem,
|
|
444
|
+
export { type ElementHintRaw, ariaSnapshotAlgorithm, findElementAlgorithm, inspectElementRaw, outlineAlgorithm, pasteValue, pickerOverlayAlgorithm, readCheckedState, readDisabledState, readLocalStorageItem, semanticDomAlgorithm, serializeRuntimeDom, writeLocalStorageItem };
|
|
@@ -4,6 +4,17 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
4
4
|
// src/semantic-dom/algorithm.ts
|
|
5
5
|
function semanticDomAlgorithm(options) {
|
|
6
6
|
const disambiguateEnabled = options?.disambiguate !== false;
|
|
7
|
+
const hiddenText = new Set(options?.hiddenTextTags ?? []);
|
|
8
|
+
function textWithoutHidden(node) {
|
|
9
|
+
if (node.nodeType === 1 && hiddenText.has(node.tagName.toLowerCase())) return "";
|
|
10
|
+
let out = "";
|
|
11
|
+
for (const child of Array.from(node.childNodes)) {
|
|
12
|
+
if (child.nodeType === 3) out += child.nodeValue ?? "";
|
|
13
|
+
else if (child.nodeType === 1) out += textWithoutHidden(child);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
__name(textWithoutHidden, "textWithoutHidden");
|
|
7
18
|
const INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
|
|
8
19
|
"a",
|
|
9
20
|
"button",
|
|
@@ -307,7 +318,7 @@ function semanticDomAlgorithm(options) {
|
|
|
307
318
|
}
|
|
308
319
|
__name(shortTextChunk, "shortTextChunk");
|
|
309
320
|
function textLocatorFallback(el, tag) {
|
|
310
|
-
const rawText2 = el
|
|
321
|
+
const rawText2 = textWithoutHidden(el);
|
|
311
322
|
const chunk = shortTextChunk(rawText2);
|
|
312
323
|
if (chunk) {
|
|
313
324
|
const escaped = chunk.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
@@ -323,7 +334,7 @@ function semanticDomAlgorithm(options) {
|
|
|
323
334
|
function buildLightNode(el, parentIdx, shadowHostSelector) {
|
|
324
335
|
const tag = el.tagName.toLowerCase();
|
|
325
336
|
const interactive = isInteractive(el);
|
|
326
|
-
let text2 = el.
|
|
337
|
+
let text2 = textWithoutHidden(el).trim().slice(0, 80);
|
|
327
338
|
if (text2.length >= 6) {
|
|
328
339
|
const half = Math.floor(text2.length / 2);
|
|
329
340
|
if (text2.slice(0, half) === text2.slice(half)) text2 = text2.slice(0, half);
|
|
@@ -613,7 +624,7 @@ function semanticDomAlgorithm(options) {
|
|
|
613
624
|
const role = landmark.getAttribute("role");
|
|
614
625
|
const ariaLabel = landmark.getAttribute("aria-label");
|
|
615
626
|
const heading = landmark.querySelector("h1, h2, h3, h4, h5, h6");
|
|
616
|
-
const headingText = heading
|
|
627
|
+
const headingText = heading ? textWithoutHidden(heading).trim().slice(0, 40) : "";
|
|
617
628
|
const label = ariaLabel?.slice(0, 40) || headingText || "";
|
|
618
629
|
const baseName = role || tag;
|
|
619
630
|
const zoneKey = label ? `${baseName}: ${label}` : `${kind}:${baseName}`;
|
|
@@ -671,7 +682,7 @@ function semanticDomAlgorithm(options) {
|
|
|
671
682
|
if (!itemEl) continue;
|
|
672
683
|
if (itemEl.querySelector('a[href*="/p/"], a[href*="/product/"], a[href*="/item/"]')) productLinkCount++;
|
|
673
684
|
if (itemEl.querySelector('[aria-label*="ktivierung" i], [aria-label*="Activate" i]')) promoCtaCount++;
|
|
674
|
-
const text2 = (itemEl
|
|
685
|
+
const text2 = textWithoutHidden(itemEl).trim();
|
|
675
686
|
if (text2.length > 0 && text2.length <= 30) chipCount++;
|
|
676
687
|
}
|
|
677
688
|
if (productLinkCount >= 5) {
|
|
@@ -770,11 +781,11 @@ function semanticDomAlgorithm(options) {
|
|
|
770
781
|
if (rect.width === 0 || rect.height === 0) continue;
|
|
771
782
|
const { kind, zoneKey, label } = getZoneAssignment(tbl);
|
|
772
783
|
registerZone(zoneKey, kind, label);
|
|
773
|
-
let headers = Array.from(tbl.querySelectorAll("thead th, thead td")).map((h) => (h
|
|
784
|
+
let headers = Array.from(tbl.querySelectorAll("thead th, thead td")).map((h) => textWithoutHidden(h).trim().slice(0, 40)).filter((t) => t);
|
|
774
785
|
if (headers.length === 0) {
|
|
775
786
|
const firstRow2 = tbl.querySelector("tr");
|
|
776
787
|
if (firstRow2) {
|
|
777
|
-
headers = Array.from(firstRow2.querySelectorAll("th")).map((h) => (h
|
|
788
|
+
headers = Array.from(firstRow2.querySelectorAll("th")).map((h) => textWithoutHidden(h).trim().slice(0, 40)).filter((t) => t);
|
|
778
789
|
}
|
|
779
790
|
}
|
|
780
791
|
const bodyRows = tbl.querySelectorAll("tbody tr").length;
|
|
@@ -904,7 +915,7 @@ function semanticDomAlgorithm(options) {
|
|
|
904
915
|
const n = nodes[ni];
|
|
905
916
|
if (n.interactive || n.exportAsActionableHost) {
|
|
906
917
|
const el = n.element;
|
|
907
|
-
const t = (el.getAttribute("title") || el
|
|
918
|
+
const t = (el.getAttribute("title") || textWithoutHidden(el)).trim();
|
|
908
919
|
if (t.length > bestText.length) {
|
|
909
920
|
bestText = t.slice(0, 80);
|
|
910
921
|
if (el.getAttribute("title")) break;
|
|
@@ -913,7 +924,7 @@ function semanticDomAlgorithm(options) {
|
|
|
913
924
|
for (const ci of n.children) stack.push(ci);
|
|
914
925
|
}
|
|
915
926
|
if (!bestText) {
|
|
916
|
-
bestText = (nodes[itemIdx].element
|
|
927
|
+
bestText = textWithoutHidden(nodes[itemIdx].element).trim().slice(0, 80);
|
|
917
928
|
}
|
|
918
929
|
itemTexts.push(bestText);
|
|
919
930
|
}
|
|
@@ -1031,7 +1042,7 @@ function semanticDomAlgorithm(options) {
|
|
|
1031
1042
|
const descendants = cursor.querySelectorAll(tag);
|
|
1032
1043
|
let textMatches = 0;
|
|
1033
1044
|
for (let di = 0; di < descendants.length; di++) {
|
|
1034
|
-
if ((descendants[di]
|
|
1045
|
+
if (textWithoutHidden(descendants[di]).includes(searchText)) textMatches++;
|
|
1035
1046
|
}
|
|
1036
1047
|
if (textMatches === 1) {
|
|
1037
1048
|
const candidate = `${anchorSel} ${initialSelector}`;
|
|
@@ -1053,7 +1064,7 @@ function semanticDomAlgorithm(options) {
|
|
|
1053
1064
|
__name(ancestorAnchorRefine, "ancestorAnchorRefine");
|
|
1054
1065
|
function hasTextRefine(el, initialSelector) {
|
|
1055
1066
|
if (/:has-text\(/.test(initialSelector)) return null;
|
|
1056
|
-
const chunk = shortTextChunk(el
|
|
1067
|
+
const chunk = shortTextChunk(textWithoutHidden(el));
|
|
1057
1068
|
if (!chunk) return null;
|
|
1058
1069
|
let nodes2;
|
|
1059
1070
|
try {
|
|
@@ -1061,7 +1072,7 @@ function semanticDomAlgorithm(options) {
|
|
|
1061
1072
|
} catch {
|
|
1062
1073
|
return null;
|
|
1063
1074
|
}
|
|
1064
|
-
const matches = nodes2.filter((n) => (n
|
|
1075
|
+
const matches = nodes2.filter((n) => textWithoutHidden(n).includes(chunk));
|
|
1065
1076
|
if (matches.length !== 1) return null;
|
|
1066
1077
|
const escapedChunk = chunk.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1067
1078
|
const refined = `${initialSelector}:has-text("${escapedChunk}")`;
|
|
@@ -2147,7 +2158,7 @@ function ariaSnapshotAlgorithm(h) {
|
|
|
2147
2158
|
const el = node;
|
|
2148
2159
|
if (!h.isVisible(el)) return;
|
|
2149
2160
|
const tag = el.tagName.toLowerCase();
|
|
2150
|
-
if (
|
|
2161
|
+
if (h.isHiddenText(el)) return;
|
|
2151
2162
|
if (tag === "iframe") {
|
|
2152
2163
|
flush();
|
|
2153
2164
|
visit(el, bucket, refPrefix, parentName);
|
|
@@ -2575,7 +2586,8 @@ __name(pickerOverlayAlgorithm, "pickerOverlayAlgorithm");
|
|
|
2575
2586
|
|
|
2576
2587
|
// src/inspection/full-html-capture.ts
|
|
2577
2588
|
import { serializeRuntimeDom as serializeRuntimeDomInjectable } from "@unotest/web/internal/page-inject";
|
|
2578
|
-
function serializeRuntimeDom() {
|
|
2589
|
+
function serializeRuntimeDom(root, opts) {
|
|
2590
|
+
globalThis.__name ??= (fn) => fn;
|
|
2579
2591
|
const VOID = /* @__PURE__ */ new Set([
|
|
2580
2592
|
"area",
|
|
2581
2593
|
"base",
|
|
@@ -2616,7 +2628,9 @@ function serializeRuntimeDom() {
|
|
|
2616
2628
|
}
|
|
2617
2629
|
__name(hiddenValue, "hiddenValue");
|
|
2618
2630
|
function shownValue(el, value) {
|
|
2619
|
-
|
|
2631
|
+
if (isSecretField(el)) return hiddenValue(value);
|
|
2632
|
+
const hiddenField = el.tagName.toLowerCase() === "input" && el.type === "hidden";
|
|
2633
|
+
return hiddenField && !opts.full ? hiddenValue(value) : value;
|
|
2620
2634
|
}
|
|
2621
2635
|
__name(shownValue, "shownValue");
|
|
2622
2636
|
function runtimeAttrs(el) {
|
|
@@ -2639,7 +2653,9 @@ function serializeRuntimeDom() {
|
|
|
2639
2653
|
if (tag === "script" || tag === "style") {
|
|
2640
2654
|
let a = "";
|
|
2641
2655
|
for (const attr of Array.from(el.attributes)) a += ` ${attr.name}="${escAttr(attr.value)}"`;
|
|
2642
|
-
|
|
2656
|
+
const text = el.textContent ?? "";
|
|
2657
|
+
const body = tag === "script" && !opts.full && text !== "" ? `/* \u2039script, ${text.length} chars\u203A */` : text;
|
|
2658
|
+
return `<${tag}${a}>${body}</${tag}>`;
|
|
2643
2659
|
}
|
|
2644
2660
|
let attrs = "";
|
|
2645
2661
|
for (const attr of Array.from(el.attributes)) {
|
|
@@ -2665,7 +2681,7 @@ function serializeRuntimeDom() {
|
|
|
2665
2681
|
return html;
|
|
2666
2682
|
}
|
|
2667
2683
|
__name(serialize, "serialize");
|
|
2668
|
-
return "<!DOCTYPE html>\n" + serialize(document.documentElement);
|
|
2684
|
+
return root === null ? "<!DOCTYPE html>\n" + serialize(document.documentElement) : serialize(root);
|
|
2669
2685
|
}
|
|
2670
2686
|
__name(serializeRuntimeDom, "serializeRuntimeDom");
|
|
2671
2687
|
|
|
@@ -2785,14 +2801,29 @@ function readLocalStorageItem(key) {
|
|
|
2785
2801
|
return localStorage.getItem(key);
|
|
2786
2802
|
}
|
|
2787
2803
|
__name(readLocalStorageItem, "readLocalStorageItem");
|
|
2788
|
-
function
|
|
2789
|
-
globalThis.__name ??= (fn) => fn;
|
|
2790
|
-
return el.outerHTML;
|
|
2791
|
-
}
|
|
2792
|
-
__name(readOuterHtml, "readOuterHtml");
|
|
2793
|
-
function inspectElementRaw(el) {
|
|
2804
|
+
function inspectElementRaw(el, args) {
|
|
2794
2805
|
globalThis.__name ??= (fn) => fn;
|
|
2795
2806
|
const e = el;
|
|
2807
|
+
const hidden = new Set(args.hiddenTextTags);
|
|
2808
|
+
function textWithoutHidden(node) {
|
|
2809
|
+
if (node.nodeType === 1 && hidden.has(node.tagName.toLowerCase())) return "";
|
|
2810
|
+
let out = "";
|
|
2811
|
+
for (const child of Array.from(node.childNodes)) {
|
|
2812
|
+
if (child.nodeType === 3) out += child.nodeValue ?? "";
|
|
2813
|
+
else if (child.nodeType === 1) out += textWithoutHidden(child);
|
|
2814
|
+
}
|
|
2815
|
+
return out;
|
|
2816
|
+
}
|
|
2817
|
+
__name(textWithoutHidden, "textWithoutHidden");
|
|
2818
|
+
function readableOf(full) {
|
|
2819
|
+
const whole = args.readable;
|
|
2820
|
+
if (whole === void 0) {
|
|
2821
|
+
const folded = full.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
2822
|
+
return folded ? { readable: folded } : {};
|
|
2823
|
+
}
|
|
2824
|
+
return full.length > whole.limit ? { readable: full.slice(0, whole.limit), readableTruncated: true } : { readable: full };
|
|
2825
|
+
}
|
|
2826
|
+
__name(readableOf, "readableOf");
|
|
2796
2827
|
const ds = e.dataset;
|
|
2797
2828
|
const testId = ds.testid ?? ds.qa ?? e.getAttribute("data-testid") ?? e.getAttribute("data-qa");
|
|
2798
2829
|
const ariaLabel = e.getAttribute("aria-label") ?? void 0;
|
|
@@ -2808,7 +2839,7 @@ function inspectElementRaw(el) {
|
|
|
2808
2839
|
const parts = [];
|
|
2809
2840
|
for (const id of ids) {
|
|
2810
2841
|
const n = e.ownerDocument?.getElementById(id);
|
|
2811
|
-
if (n) parts.push((n
|
|
2842
|
+
if (n) parts.push(textWithoutHidden(n).trim());
|
|
2812
2843
|
}
|
|
2813
2844
|
name = parts.filter(Boolean).join(" ") || void 0;
|
|
2814
2845
|
}
|
|
@@ -2844,11 +2875,11 @@ function inspectElementRaw(el) {
|
|
|
2844
2875
|
"col"
|
|
2845
2876
|
]);
|
|
2846
2877
|
const structural = STRUCTURAL_TAGS.has(tag);
|
|
2847
|
-
const rawText = structural ? "" : (e
|
|
2878
|
+
const rawText = structural ? "" : textWithoutHidden(e).replace(/\s+/g, " ").trim().slice(0, 200);
|
|
2848
2879
|
const text = rawText || void 0;
|
|
2849
2880
|
if (!name && text) name = text;
|
|
2850
2881
|
const CONTAINER_TEXT_CAP = 120;
|
|
2851
|
-
const containerFull = structural && !name ? (e
|
|
2882
|
+
const containerFull = structural && !name ? textWithoutHidden(e).replace(/\s+/g, " ").trim() : "";
|
|
2852
2883
|
const containerText = containerFull && containerFull.length <= CONTAINER_TEXT_CAP ? containerFull : void 0;
|
|
2853
2884
|
const href = tag === "a" ? e.getAttribute("href") ?? void 0 : void 0;
|
|
2854
2885
|
const placeholder = e.placeholder?.trim() || void 0;
|
|
@@ -2930,6 +2961,7 @@ function inspectElementRaw(el) {
|
|
|
2930
2961
|
titleAttr,
|
|
2931
2962
|
text,
|
|
2932
2963
|
containerText,
|
|
2964
|
+
...readableOf(textWithoutHidden(e)),
|
|
2933
2965
|
href,
|
|
2934
2966
|
elementId,
|
|
2935
2967
|
nameAttr,
|
|
@@ -2976,7 +3008,6 @@ export {
|
|
|
2976
3008
|
readCheckedState,
|
|
2977
3009
|
readDisabledState,
|
|
2978
3010
|
readLocalStorageItem,
|
|
2979
|
-
readOuterHtml,
|
|
2980
3011
|
semanticDomAlgorithm,
|
|
2981
3012
|
serializeRuntimeDom,
|
|
2982
3013
|
writeLocalStorageItem
|
|
@@ -91,13 +91,27 @@ interface LaunchOptions {
|
|
|
91
91
|
/** Extra browser command-line arguments (`config.launch.args`). */
|
|
92
92
|
args?: string[];
|
|
93
93
|
}
|
|
94
|
-
interface
|
|
94
|
+
interface ViewportSize {
|
|
95
95
|
width: number;
|
|
96
96
|
height: number;
|
|
97
97
|
}
|
|
98
|
+
/** What a context is asked to look like. Each field left out comes from
|
|
99
|
+
* the device preset, or from DEFAULT_VIEWPORT when there is none. */
|
|
100
|
+
interface Viewport extends Partial<ViewportSize> {
|
|
101
|
+
/** Device pixels per CSS pixel. Fixed for the life of a context. */
|
|
102
|
+
deviceScaleFactor?: number;
|
|
103
|
+
}
|
|
98
104
|
interface ContextOptions {
|
|
99
105
|
baseUrl?: string;
|
|
106
|
+
/** A device preset by name — the driver owns the list and refuses a
|
|
107
|
+
* name it does not know. Applied before `viewport`, which overrides the
|
|
108
|
+
* preset's size and scale field by field. */
|
|
109
|
+
device?: string;
|
|
100
110
|
viewport?: Viewport;
|
|
111
|
+
/** Put a fake clock on the context before its first page loads, so that
|
|
112
|
+
* every timer the pages set is under `DriverContext.clock()`. Off by
|
|
113
|
+
* default: time on the page is the real one. */
|
|
114
|
+
clock?: boolean;
|
|
101
115
|
/** Path to storageState JSON (D-23 caching, optional). */
|
|
102
116
|
storageState?: string;
|
|
103
117
|
/** Per-context override for dialog policy. Defaults to config.dialogPolicy. */
|
|
@@ -231,11 +245,34 @@ interface ScreenshotOptions {
|
|
|
231
245
|
* as `png`. `webp` needs Playwright 1.62+, which is why the package
|
|
232
246
|
* pins that floor. */
|
|
233
247
|
type?: "png" | "jpeg" | "webp";
|
|
248
|
+
/** `"disabled"` — CSS animations and transitions are finished first, so
|
|
249
|
+
* two captures of the same page are the same picture (the visual
|
|
250
|
+
* assert). Default: as Playwright, animations run. */
|
|
251
|
+
animations?: "disabled" | "allow";
|
|
252
|
+
/** `"hide"` — the text caret is not drawn (it blinks). Default `"initial"`. */
|
|
253
|
+
caret?: "hide" | "initial";
|
|
234
254
|
}
|
|
235
255
|
/** Encoding options for an element capture — `ScreenshotOptions` minus
|
|
236
256
|
* the page-geometry fields (`fullPage`, `clip`) that have no meaning for a
|
|
237
257
|
* single element. */
|
|
238
|
-
type ElementScreenshotOptions = Pick<ScreenshotOptions, "type" | "quality">;
|
|
258
|
+
type ElementScreenshotOptions = Pick<ScreenshotOptions, "type" | "quality" | "animations" | "caret">;
|
|
259
|
+
/** A rectangle in the PIXELS of a capture (device pixels, the capture's own
|
|
260
|
+
* origin) — where an element lies in a screenshot. */
|
|
261
|
+
interface CaptureRect {
|
|
262
|
+
x: number;
|
|
263
|
+
y: number;
|
|
264
|
+
width: number;
|
|
265
|
+
height: number;
|
|
266
|
+
}
|
|
267
|
+
/** Which capture a `CaptureRect` is measured in. */
|
|
268
|
+
type CaptureFrame = {
|
|
269
|
+
kind: "viewport";
|
|
270
|
+
} | {
|
|
271
|
+
kind: "fullPage";
|
|
272
|
+
} | {
|
|
273
|
+
kind: "element";
|
|
274
|
+
loc: LocatorValue;
|
|
275
|
+
};
|
|
239
276
|
interface CookieOptions {
|
|
240
277
|
domain?: string;
|
|
241
278
|
path?: string;
|
|
@@ -250,7 +287,12 @@ interface CookieOptions {
|
|
|
250
287
|
* dismisses every dialog on behalf of the client that has none — and a
|
|
251
288
|
* second client watching the run's page would answer `confirm()` with
|
|
252
289
|
* "no" before the run's own accept lands. */
|
|
253
|
-
|
|
290
|
+
interface AttachOptions extends Pick<ContextOptions, "dialogPolicy"> {
|
|
291
|
+
/** The browser's id of the context to attach to (CDP `browserContextId`,
|
|
292
|
+
* as the run publishes it). Unset, or not found → the first context
|
|
293
|
+
* with an open page, as before. */
|
|
294
|
+
contextId?: string;
|
|
295
|
+
}
|
|
254
296
|
|
|
255
297
|
interface WebDriver {
|
|
256
298
|
/**
|
|
@@ -280,7 +322,7 @@ interface WebDriver {
|
|
|
280
322
|
*/
|
|
281
323
|
wsEndpoint(): string | undefined;
|
|
282
324
|
connectShared(wsEndpoint: string, browser: LaunchOptions["browser"]): Promise<void>;
|
|
283
|
-
attachExisting(opts?: AttachOptions): DriverContext
|
|
325
|
+
attachExisting(opts?: AttachOptions): Promise<DriverContext>;
|
|
284
326
|
}
|
|
285
327
|
/** One captured network request/response, recorded only under
|
|
286
328
|
* `UNOTEST_NETWORK`. `status`/`ok` come from the response; `failureText`
|
|
@@ -299,6 +341,21 @@ interface NetworkEntry {
|
|
|
299
341
|
durationMs?: number;
|
|
300
342
|
timestamp: string;
|
|
301
343
|
}
|
|
344
|
+
/** A fake clock shared by every page of a context. Time runs by itself
|
|
345
|
+
* until `freeze`, and again after `run`. */
|
|
346
|
+
interface DriverClock {
|
|
347
|
+
/** Stop time at `at`: `Date` reads it, and no timer fires until `tick`
|
|
348
|
+
* or `run`. Any instant, past or future — no timer fires on the way. */
|
|
349
|
+
freeze(at: Date): Promise<void>;
|
|
350
|
+
/** Advance frozen time by `ms`, firing every timer due in it. */
|
|
351
|
+
tick(ms: number): Promise<void>;
|
|
352
|
+
/** Let time run again from where it stands. */
|
|
353
|
+
run(): Promise<void>;
|
|
354
|
+
/** Whether time stands still now (after `freeze`, before `run`). While
|
|
355
|
+
* it does, `requestAnimationFrame` does not fire either — a wait for a
|
|
356
|
+
* painted frame would wait for nothing. */
|
|
357
|
+
frozen(): boolean;
|
|
358
|
+
}
|
|
302
359
|
interface DriverContext {
|
|
303
360
|
/** Currently-open tabs/windows in this context. Index 0 = first opened. */
|
|
304
361
|
pages(): ReadonlyArray<DriverPage>;
|
|
@@ -334,6 +391,22 @@ interface DriverContext {
|
|
|
334
391
|
* before auto-switching to a popup (frame scope does not survive tab
|
|
335
392
|
* changes). */
|
|
336
393
|
frameDepth(): number;
|
|
394
|
+
/**
|
|
395
|
+
* Resize every open page of this context, and every page it opens after
|
|
396
|
+
* this call. Width and height only: the device scale factor is fixed when
|
|
397
|
+
* the context is created — changing it would mean a new context and the
|
|
398
|
+
* loss of the page, which a resize must never cost.
|
|
399
|
+
*/
|
|
400
|
+
setViewport(size: ViewportSize): Promise<void>;
|
|
401
|
+
/** The browser's own id of this context (CDP `browserContextId`), the
|
|
402
|
+
* same in every process connected to the browser — or null where the
|
|
403
|
+
* engine has no such id to give (Firefox, WebKit) or no page is open. */
|
|
404
|
+
browserContextId(): Promise<string | null>;
|
|
405
|
+
/** The page clock. Works only on a context opened with `clock: true`
|
|
406
|
+
* (the fake clock must be in place before the first page loads, or the
|
|
407
|
+
* page's own timers stay real) — otherwise every call throws
|
|
408
|
+
* ClockNotInstalledError. */
|
|
409
|
+
clock(): DriverClock;
|
|
337
410
|
/** Typed storage shortcuts (D-6). */
|
|
338
411
|
setLocalStorage(key: string, value: string): Promise<void>;
|
|
339
412
|
getLocalStorage(key: string): Promise<string | null>;
|
|
@@ -443,11 +516,19 @@ interface DriverPage {
|
|
|
443
516
|
* viewport — the evidence shot of a card / row / dialog. Same encoding
|
|
444
517
|
* options as `screenshot`; `fullPage` / `clip` don't apply. */
|
|
445
518
|
screenshotElement(loc: LocatorValue, opts?: ElementScreenshotOptions): Promise<Buffer>;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
519
|
+
/** Where each element lies in a capture of `frame`, in that capture's
|
|
520
|
+
* pixels (device pixel ratio, page scroll and the element's own box
|
|
521
|
+
* applied) — the masks of the visual assert. Each locator must match
|
|
522
|
+
* exactly one element (a locator that matches none is an error, not a
|
|
523
|
+
* skipped mask); an element that is not rendered has no box and is
|
|
524
|
+
* left out. */
|
|
525
|
+
boxesInCapture(locs: readonly LocatorValue[], frame: CaptureFrame): Promise<CaptureRect[]>;
|
|
526
|
+
inspectElement(loc: LocatorValue, opts?: InspectElementOptions): Promise<ElementHint | null>;
|
|
527
|
+
/** HTML of the element the locator matches (first match), redacted like
|
|
528
|
+
* page.html — secret and hidden field values and script content become
|
|
529
|
+
* labels — or null when it resolves to zero elements. Used by debug
|
|
530
|
+
* logging to persist "what did this locator actually resolve to" as
|
|
531
|
+
* agent-readable proof — NOT part of the DSL / action surface. */
|
|
451
532
|
outerHTML(loc: LocatorValue): Promise<string | null>;
|
|
452
533
|
}
|
|
453
534
|
/**
|
|
@@ -457,6 +538,15 @@ interface DriverPage {
|
|
|
457
538
|
* optional — an element might have only a role+name, or only a testId,
|
|
458
539
|
* or nothing stable at all (in which case the resolver throws).
|
|
459
540
|
*/
|
|
541
|
+
/** How `inspectElement` reads the element's `readable` text. By default
|
|
542
|
+
* whitespace folded and clipped to 200 — a hint. `whole` keeps the text as
|
|
543
|
+
* it is, up to `limit` characters, for a refusal that quotes it (#256). */
|
|
544
|
+
interface InspectElementOptions {
|
|
545
|
+
readable?: {
|
|
546
|
+
whole: true;
|
|
547
|
+
limit: number;
|
|
548
|
+
};
|
|
549
|
+
}
|
|
460
550
|
interface ElementHint {
|
|
461
551
|
/** The wire-only ref the resolver is acting on. Echoed back so the
|
|
462
552
|
* resolver can verify the resolved locator still points at the same
|
|
@@ -496,6 +586,14 @@ interface ElementHint {
|
|
|
496
586
|
* a record). Kept separate from `text` so ARIA name semantics (a
|
|
497
587
|
* container takes its name only from aria-label/labelledby) hold. */
|
|
498
588
|
containerText?: string;
|
|
589
|
+
/** What a reader sees of the element's text, for any element, container
|
|
590
|
+
* or not: its text without script, style, template and noscript content
|
|
591
|
+
* (`TEXT_HIDDEN_TAGS`), whitespace folded, clipped to 200. What
|
|
592
|
+
* `check_locator` shows as where a locator points. */
|
|
593
|
+
readable?: string;
|
|
594
|
+
/** Set when `readable` was asked for whole and the text was longer than
|
|
595
|
+
* the limit: `readable` is then its beginning. */
|
|
596
|
+
readableTruncated?: true;
|
|
499
597
|
/** href attribute for `<a>` elements lacking any of the above.
|
|
500
598
|
* Resolver emits `locator('a[href="..."]')`. */
|
|
501
599
|
href?: string;
|
|
@@ -547,4 +645,4 @@ interface AncestorHint {
|
|
|
547
645
|
className?: string;
|
|
548
646
|
}
|
|
549
647
|
|
|
550
|
-
export { type AttachOptions as A, type
|
|
648
|
+
export { type AttachOptions as A, type CaptureFrame as C, type DriverContext as D, type ElementHint as E, type FillOptions as F, type GotoOptions as G, type HoverOptions as H, type LocatorValue as L, type NetworkEntry as N, type PressOptions as P, type ReloadOptions as R, type ScreenshotOptions as S, type Viewport as V, type WebDriver as W, type CaptureRect as a, type LaunchOptions as b, type ContextOptions as c, type ActionTimeout as d, type CheckOptions as e, type ClickOptions as f, type CookieOptions as g, type DriverPage as h, type NavigationOptions as i, type SelectOptions as j, type ViewportSize as k, type WaitOptions as l, type WaitState as m, type LocatorStep as n, appendStep as o, describeLocator as p, isLocatorValue as q, rootedAt as r };
|