@unotest/web 0.43.0 → 0.43.2
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 +131 -3
- package/dist/config/schema.d.ts +50 -34
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +2 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +8 -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 +23 -6
- package/dist/inspection/page-inject.js +83 -28
- package/dist/{interfaces-7nMNCr2h.d.ts → interfaces-ClW2IRgY.d.ts} +23 -5
- 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 +36 -1
- package/guides/dsl-reference.md +8 -0
- package/package.json +8 -8
|
@@ -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-ClW2IRgY.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);
|
|
@@ -2418,11 +2429,19 @@ function pickerOverlayAlgorithm(op) {
|
|
|
2418
2429
|
...extra.key !== void 0 ? { key: extra.key } : {}
|
|
2419
2430
|
});
|
|
2420
2431
|
}, "push");
|
|
2432
|
+
const SECRET_AUTOCOMPLETE = ["current-password", "new-password", "one-time-code"];
|
|
2433
|
+
const isSecretField = /* @__PURE__ */ __name((el) => {
|
|
2434
|
+
if ((el.getAttribute("type") ?? "").trim().toLowerCase() === "password") return true;
|
|
2435
|
+
const tokens = (el.getAttribute("autocomplete") ?? "").toLowerCase().split(/\s+/);
|
|
2436
|
+
return tokens.some((token) => SECRET_AUTOCOMPLETE.includes(token));
|
|
2437
|
+
}, "isSecretField");
|
|
2438
|
+
const hiddenValue = /* @__PURE__ */ __name((value) => `\u2039hidden, ${value.length} chars\u203A`, "hiddenValue");
|
|
2421
2439
|
const commitFill = /* @__PURE__ */ __name(() => {
|
|
2422
2440
|
const node = state.pendingFill;
|
|
2423
2441
|
state.pendingFill = null;
|
|
2424
2442
|
if (!node) return;
|
|
2425
|
-
const
|
|
2443
|
+
const typed = node.value ?? "";
|
|
2444
|
+
const value = isSecretField(node) ? hiddenValue(typed) : typed;
|
|
2426
2445
|
push(node, "fill", { placement: "append", value });
|
|
2427
2446
|
}, "commitFill");
|
|
2428
2447
|
const describe = /* @__PURE__ */ __name((node) => {
|
|
@@ -2567,7 +2586,8 @@ __name(pickerOverlayAlgorithm, "pickerOverlayAlgorithm");
|
|
|
2567
2586
|
|
|
2568
2587
|
// src/inspection/full-html-capture.ts
|
|
2569
2588
|
import { serializeRuntimeDom as serializeRuntimeDomInjectable } from "@unotest/web/internal/page-inject";
|
|
2570
|
-
function serializeRuntimeDom() {
|
|
2589
|
+
function serializeRuntimeDom(root, opts) {
|
|
2590
|
+
globalThis.__name ??= (fn) => fn;
|
|
2571
2591
|
const VOID = /* @__PURE__ */ new Set([
|
|
2572
2592
|
"area",
|
|
2573
2593
|
"base",
|
|
@@ -2596,6 +2616,23 @@ function serializeRuntimeDom() {
|
|
|
2596
2616
|
return tag === "input" && (name === "value" || name === "checked");
|
|
2597
2617
|
}
|
|
2598
2618
|
__name(isLiveControlledAttr, "isLiveControlledAttr");
|
|
2619
|
+
const SECRET_AUTOCOMPLETE = ["current-password", "new-password", "one-time-code"];
|
|
2620
|
+
function isSecretField(el) {
|
|
2621
|
+
if ((el.getAttribute("type") ?? "").trim().toLowerCase() === "password") return true;
|
|
2622
|
+
const tokens = (el.getAttribute("autocomplete") ?? "").toLowerCase().split(/\s+/);
|
|
2623
|
+
return tokens.some((token) => SECRET_AUTOCOMPLETE.includes(token));
|
|
2624
|
+
}
|
|
2625
|
+
__name(isSecretField, "isSecretField");
|
|
2626
|
+
function hiddenValue(value) {
|
|
2627
|
+
return `\u2039hidden, ${value.length} chars\u203A`;
|
|
2628
|
+
}
|
|
2629
|
+
__name(hiddenValue, "hiddenValue");
|
|
2630
|
+
function shownValue(el, value) {
|
|
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;
|
|
2634
|
+
}
|
|
2635
|
+
__name(shownValue, "shownValue");
|
|
2599
2636
|
function runtimeAttrs(el) {
|
|
2600
2637
|
const tag = el.tagName.toLowerCase();
|
|
2601
2638
|
if (tag !== "input") return "";
|
|
@@ -2604,7 +2641,7 @@ function serializeRuntimeDom() {
|
|
|
2604
2641
|
if (type === "checkbox" || type === "radio") {
|
|
2605
2642
|
return inp.checked ? " checked" : "";
|
|
2606
2643
|
}
|
|
2607
|
-
return ` value="${escAttr(inp.value)}"`;
|
|
2644
|
+
return ` value="${escAttr(shownValue(el, inp.value))}"`;
|
|
2608
2645
|
}
|
|
2609
2646
|
__name(runtimeAttrs, "runtimeAttrs");
|
|
2610
2647
|
function serialize(node) {
|
|
@@ -2616,7 +2653,9 @@ function serializeRuntimeDom() {
|
|
|
2616
2653
|
if (tag === "script" || tag === "style") {
|
|
2617
2654
|
let a = "";
|
|
2618
2655
|
for (const attr of Array.from(el.attributes)) a += ` ${attr.name}="${escAttr(attr.value)}"`;
|
|
2619
|
-
|
|
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}>`;
|
|
2620
2659
|
}
|
|
2621
2660
|
let attrs = "";
|
|
2622
2661
|
for (const attr of Array.from(el.attributes)) {
|
|
@@ -2624,6 +2663,7 @@ function serializeRuntimeDom() {
|
|
|
2624
2663
|
attrs += ` ${attr.name}="${escAttr(attr.value)}"`;
|
|
2625
2664
|
}
|
|
2626
2665
|
attrs += runtimeAttrs(el);
|
|
2666
|
+
if (tag === "noscript") return `<${tag}${attrs}></${tag}>`;
|
|
2627
2667
|
let html = `<${tag}${attrs}>`;
|
|
2628
2668
|
if (VOID.has(tag)) return html;
|
|
2629
2669
|
const sr = el.shadowRoot;
|
|
@@ -2633,7 +2673,7 @@ function serializeRuntimeDom() {
|
|
|
2633
2673
|
html += `</template>`;
|
|
2634
2674
|
}
|
|
2635
2675
|
if (tag === "textarea") {
|
|
2636
|
-
html += escText(el.value);
|
|
2676
|
+
html += escText(shownValue(el, el.value));
|
|
2637
2677
|
} else {
|
|
2638
2678
|
for (const c of Array.from(el.childNodes)) html += serialize(c);
|
|
2639
2679
|
}
|
|
@@ -2641,7 +2681,7 @@ function serializeRuntimeDom() {
|
|
|
2641
2681
|
return html;
|
|
2642
2682
|
}
|
|
2643
2683
|
__name(serialize, "serialize");
|
|
2644
|
-
return "<!DOCTYPE html>\n" + serialize(document.documentElement);
|
|
2684
|
+
return root === null ? "<!DOCTYPE html>\n" + serialize(document.documentElement) : serialize(root);
|
|
2645
2685
|
}
|
|
2646
2686
|
__name(serializeRuntimeDom, "serializeRuntimeDom");
|
|
2647
2687
|
|
|
@@ -2761,14 +2801,29 @@ function readLocalStorageItem(key) {
|
|
|
2761
2801
|
return localStorage.getItem(key);
|
|
2762
2802
|
}
|
|
2763
2803
|
__name(readLocalStorageItem, "readLocalStorageItem");
|
|
2764
|
-
function
|
|
2765
|
-
globalThis.__name ??= (fn) => fn;
|
|
2766
|
-
return el.outerHTML;
|
|
2767
|
-
}
|
|
2768
|
-
__name(readOuterHtml, "readOuterHtml");
|
|
2769
|
-
function inspectElementRaw(el) {
|
|
2804
|
+
function inspectElementRaw(el, args) {
|
|
2770
2805
|
globalThis.__name ??= (fn) => fn;
|
|
2771
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");
|
|
2772
2827
|
const ds = e.dataset;
|
|
2773
2828
|
const testId = ds.testid ?? ds.qa ?? e.getAttribute("data-testid") ?? e.getAttribute("data-qa");
|
|
2774
2829
|
const ariaLabel = e.getAttribute("aria-label") ?? void 0;
|
|
@@ -2784,7 +2839,7 @@ function inspectElementRaw(el) {
|
|
|
2784
2839
|
const parts = [];
|
|
2785
2840
|
for (const id of ids) {
|
|
2786
2841
|
const n = e.ownerDocument?.getElementById(id);
|
|
2787
|
-
if (n) parts.push((n
|
|
2842
|
+
if (n) parts.push(textWithoutHidden(n).trim());
|
|
2788
2843
|
}
|
|
2789
2844
|
name = parts.filter(Boolean).join(" ") || void 0;
|
|
2790
2845
|
}
|
|
@@ -2820,11 +2875,11 @@ function inspectElementRaw(el) {
|
|
|
2820
2875
|
"col"
|
|
2821
2876
|
]);
|
|
2822
2877
|
const structural = STRUCTURAL_TAGS.has(tag);
|
|
2823
|
-
const rawText = structural ? "" : (e
|
|
2878
|
+
const rawText = structural ? "" : textWithoutHidden(e).replace(/\s+/g, " ").trim().slice(0, 200);
|
|
2824
2879
|
const text = rawText || void 0;
|
|
2825
2880
|
if (!name && text) name = text;
|
|
2826
2881
|
const CONTAINER_TEXT_CAP = 120;
|
|
2827
|
-
const containerFull = structural && !name ? (e
|
|
2882
|
+
const containerFull = structural && !name ? textWithoutHidden(e).replace(/\s+/g, " ").trim() : "";
|
|
2828
2883
|
const containerText = containerFull && containerFull.length <= CONTAINER_TEXT_CAP ? containerFull : void 0;
|
|
2829
2884
|
const href = tag === "a" ? e.getAttribute("href") ?? void 0 : void 0;
|
|
2830
2885
|
const placeholder = e.placeholder?.trim() || void 0;
|
|
@@ -2906,6 +2961,7 @@ function inspectElementRaw(el) {
|
|
|
2906
2961
|
titleAttr,
|
|
2907
2962
|
text,
|
|
2908
2963
|
containerText,
|
|
2964
|
+
...readableOf(textWithoutHidden(e)),
|
|
2909
2965
|
href,
|
|
2910
2966
|
elementId,
|
|
2911
2967
|
nameAttr,
|
|
@@ -2952,7 +3008,6 @@ export {
|
|
|
2952
3008
|
readCheckedState,
|
|
2953
3009
|
readDisabledState,
|
|
2954
3010
|
readLocalStorageItem,
|
|
2955
|
-
readOuterHtml,
|
|
2956
3011
|
semanticDomAlgorithm,
|
|
2957
3012
|
serializeRuntimeDom,
|
|
2958
3013
|
writeLocalStorageItem
|
|
@@ -443,11 +443,12 @@ interface DriverPage {
|
|
|
443
443
|
* viewport — the evidence shot of a card / row / dialog. Same encoding
|
|
444
444
|
* options as `screenshot`; `fullPage` / `clip` don't apply. */
|
|
445
445
|
screenshotElement(loc: LocatorValue, opts?: ElementScreenshotOptions): Promise<Buffer>;
|
|
446
|
-
inspectElement(loc: LocatorValue): Promise<ElementHint | null>;
|
|
447
|
-
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
446
|
+
inspectElement(loc: LocatorValue, opts?: InspectElementOptions): Promise<ElementHint | null>;
|
|
447
|
+
/** HTML of the element the locator matches (first match), redacted like
|
|
448
|
+
* page.html — secret and hidden field values and script content become
|
|
449
|
+
* labels — or null when it resolves to zero elements. Used by debug
|
|
450
|
+
* logging to persist "what did this locator actually resolve to" as
|
|
451
|
+
* agent-readable proof — NOT part of the DSL / action surface. */
|
|
451
452
|
outerHTML(loc: LocatorValue): Promise<string | null>;
|
|
452
453
|
}
|
|
453
454
|
/**
|
|
@@ -457,6 +458,15 @@ interface DriverPage {
|
|
|
457
458
|
* optional — an element might have only a role+name, or only a testId,
|
|
458
459
|
* or nothing stable at all (in which case the resolver throws).
|
|
459
460
|
*/
|
|
461
|
+
/** How `inspectElement` reads the element's `readable` text. By default
|
|
462
|
+
* whitespace folded and clipped to 200 — a hint. `whole` keeps the text as
|
|
463
|
+
* it is, up to `limit` characters, for a refusal that quotes it (#256). */
|
|
464
|
+
interface InspectElementOptions {
|
|
465
|
+
readable?: {
|
|
466
|
+
whole: true;
|
|
467
|
+
limit: number;
|
|
468
|
+
};
|
|
469
|
+
}
|
|
460
470
|
interface ElementHint {
|
|
461
471
|
/** The wire-only ref the resolver is acting on. Echoed back so the
|
|
462
472
|
* resolver can verify the resolved locator still points at the same
|
|
@@ -496,6 +506,14 @@ interface ElementHint {
|
|
|
496
506
|
* a record). Kept separate from `text` so ARIA name semantics (a
|
|
497
507
|
* container takes its name only from aria-label/labelledby) hold. */
|
|
498
508
|
containerText?: string;
|
|
509
|
+
/** What a reader sees of the element's text, for any element, container
|
|
510
|
+
* or not: its text without script, style, template and noscript content
|
|
511
|
+
* (`TEXT_HIDDEN_TAGS`), whitespace folded, clipped to 200. What
|
|
512
|
+
* `check_locator` shows as where a locator points. */
|
|
513
|
+
readable?: string;
|
|
514
|
+
/** Set when `readable` was asked for whole and the text was longer than
|
|
515
|
+
* the limit: `readable` is then its beginning. */
|
|
516
|
+
readableTruncated?: true;
|
|
499
517
|
/** href attribute for `<a>` elements lacking any of the above.
|
|
500
518
|
* Resolver emits `locator('a[href="..."]')`. */
|
|
501
519
|
href?: string;
|