/** * Perception: one compact semantic snapshot of a page. * * Written fresh for the new core (D34). The behaviours below are deliberately * ported from the old system because they were learned from real pages, not * because the code was reusable: * - editor-like elements are collected even when zero-sized (Monaco, CodeMirror) * - Monaco values come from the editor API, not the DOM text * - password values are redacted at the source, never leaving the page as plaintext * - refs are assigned in document order and re-tagged on every observation */ import type { Page } from "playwright"; import { compactControls, diffControls } from "./diff.ts"; import { shortId } from "./ids.ts"; import { displayControlName } from "./accessible-name.ts"; import type { Control, Observation } from "./types.ts"; /** Attribute used to address controls. Distinct from the old system's marker. */ export const REF_ATTR = "data-core-ref"; interface Collected { url: string; title: string; controls: Control[]; dialogs: string[]; errors: string[]; heading?: string; stats?: Array<{ label: string; value: string }>; } const COLLECT = `(() => { const REF_ATTR = "data-core-ref"; const selector = [ "a", "button", "input", "select", "textarea", '[role="button"]', '[role="link"]', '[role="textbox"]', '[role="checkbox"]', '[role="radio"]', '[role="option"]', '[role="listbox"]', '[role="combobox"]', '[role="menuitem"]', '[role="tab"]', '[contenteditable="true"]', ].join(","); /* * A ref belongs to an element for as long as the element lasts. * * Refs used to be positional and reassigned from scratch on every look, so inserting * one row at the top of a list renumbered every row below it. That is why the card has * to say refs go stale, and it is what makes it impossible to describe a page as a * change from the last one: an unchanged control cannot be left out of a snapshot if * leaving it out also takes away the only way to address it. * * The marker is already in the DOM, so keeping it is enough. New elements are numbered * above every ref the page is already carrying, so a fresh number never collides with * one the model is still holding. Navigation replaces the document and the numbering * starts again, which is correct: that is a different page. */ let seq = 0; for (const el of document.querySelectorAll("[" + REF_ATTR + "]")) { const existing = Number(String(el.getAttribute(REF_ATTR)).slice(1)); if (Number.isFinite(existing) && existing > seq) seq = existing; } const visible = (el) => { const style = window.getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") { return false; } const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; // Zero-sized editors are real and must survive: Monaco hides its textarea. const editorLike = (el) => el.tagName.toLowerCase() === "textarea" || el.isContentEditable === true || el.getAttribute("role") === "textbox"; const clean = (s) => (s || "").trim().replace(/\\s+/g, " "); // A label wrapping its control also contains that control's text, so // "