101 lines · 4.2 KB · TypeScript
RawDownload
// Browser-side scripts injected via Runtime.evaluate. Authored as plain string// templates (not stringified TS functions) on purpose: esbuild/tsx instruments// nested named functions with `__name()` calls for stack traces, which are not// defined in the page realm and would throw a ReferenceError when shipped over// CDP. Raw strings are immune to that transform.//// Snapshot/click/fill approach adapted from Ghostex (MIT, maddada). export type SnapshotElement = {  ref: string;  role: string;  label: string;  selector: string;  value?: string;  disabled?: boolean;}; export type SnapshotResult = { url: string; title: string; elements: SnapshotElement[] }; // Each script is an arrow function expression; callers invoke it as// `(${SCRIPT})(...JSON.stringify(args))`. // Walk visible interactive elements and assign stable @e1..@eN refs with a CSS// selector for each. Returns { url, title, elements }.export const SNAPSHOT_SCRIPT = `(limit) => {  const selectors = [    "a[href]", "button", "input", "textarea", "select",    "[role]", "[onclick]", "[contenteditable='true']",    "[tabindex]:not([tabindex='-1'])"  ].join(",");  const cssPath = (element) => {    const parts = [];    let cursor = element;    while (cursor && cursor.nodeType === 1 && cursor !== document.documentElement) {      let part = cursor.nodeName.toLowerCase();      if (cursor.id) { parts.unshift(part + "#" + CSS.escape(cursor.id)); break; }      const parent = cursor.parentElement;      if (parent) {        const siblings = Array.from(parent.children).filter((child) => child.nodeName === cursor.nodeName);        if (siblings.length > 1) part += ":nth-of-type(" + (siblings.indexOf(cursor) + 1) + ")";      }      parts.unshift(part);      cursor = parent;    }    return parts.join(" > ");  };  const labelFor = (element) => {    const aria = element.getAttribute("aria-label");    if (aria) return aria.trim();    if (element.id) {      const label = document.querySelector('label[for="' + CSS.escape(element.id) + '"]');      if (label && label.innerText) return label.innerText.trim();    }    return (element.innerText || element.value || element.placeholder || element.title || "").trim().replace(/\\s+/g, " ");  };  const elements = [];  for (const element of Array.from(document.querySelectorAll(selectors))) {    const rect = element.getBoundingClientRect();    const style = window.getComputedStyle(element);    if (rect.width <= 0 || rect.height <= 0 || style.visibility === "hidden" || style.display === "none") continue;    elements.push({      ref: "@e" + (elements.length + 1),      role: element.getAttribute("role") || element.nodeName.toLowerCase(),      label: labelFor(element).slice(0, 240),      selector: cssPath(element),      value: ("value" in element) ? String(element.value == null ? "" : element.value).slice(0, 240) : undefined,      disabled: Boolean(element.disabled || element.getAttribute("aria-disabled") === "true")    });    if (elements.length >= limit) break;  }  return { url: location.href, title: document.title, elements };}`; // Scroll into view, focus, and click an element by selector. Returns { found }.export const CLICK_SCRIPT = `(selector) => {  const element = document.querySelector(selector);  if (!element) return { found: false };  element.scrollIntoView({ block: "center", inline: "center" });  if (element.focus) element.focus();  element.click();  return { found: true };}`; // Fill an input/textarea/select/contenteditable by selector, dispatching the// input + change events frameworks rely on. Returns { found }.export const FILL_SCRIPT = `(selector, value) => {  const element = document.querySelector(selector);  if (!element) return { found: false };  element.scrollIntoView({ block: "center", inline: "center" });  if (element.focus) element.focus();  if (element.isContentEditable) {    element.textContent = value;  } else if (element.tagName === "SELECT" || ("value" in element)) {    element.value = value;  } else {    return { found: false };  }  element.dispatchEvent(new Event("input", { bubbles: true }));  element.dispatchEvent(new Event("change", { bubbles: true }));  return { found: true };}`;