@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.
@@ -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-7nMNCr2h.js';
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
- declare function serializeRuntimeDom(): string;
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): ElementHintRaw;
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, readOuterHtml, semanticDomAlgorithm, serializeRuntimeDom, writeLocalStorageItem };
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.textContent || "";
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.textContent?.trim().slice(0, 80) || "";
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?.textContent?.trim().slice(0, 40) || "";
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.textContent || "").trim();
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.textContent || "").trim().slice(0, 40)).filter((t) => t);
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.textContent || "").trim().slice(0, 40)).filter((t) => t);
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.textContent || "").trim();
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.textContent || "").trim().slice(0, 80);
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].textContent || "").includes(searchText)) textMatches++;
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.textContent || "");
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.textContent || "").includes(chunk));
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 (tag === "script" || tag === "style" || tag === "noscript") return;
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 value = node.value ?? "";
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
- return `<${tag}${a}>${el.textContent ?? ""}</${tag}>`;
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 readOuterHtml(el) {
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.textContent ?? "").trim());
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.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
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.textContent ?? "").replace(/\s+/g, " ").trim() : "";
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
- /** outerHTML of the element the locator matches (first match), or null
448
- * when it resolves to zero elements. Used by debug logging to persist
449
- * "what did this locator actually resolve to" as agent-readable proof —
450
- * NOT part of the DSL / action surface. */
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;