import { finder } from "@medv/finder"; import { getCssSelector } from "css-selector-generator"; import optimalSelectDefault, { select as optimalSelectNamed } from "optimal-select"; import { computeAccessibleDescription, computeAccessibleName, getRole, isInaccessible, } from "dom-accessibility-api"; export type ProbeTargetResult = { readonly target_key: string; readonly css: { readonly custom_stable: SelectorCandidate | null; readonly custom_absolute: SelectorCandidate | null; readonly finder: SelectorCandidate | null; readonly css_selector_generator: SelectorCandidate | null; readonly optimal_select: SelectorCandidate | null; }; readonly xpath: { readonly stable: XPathCandidate | null; readonly absolute: XPathCandidate | null; }; readonly accessibility: AccessibilityCandidate | null; readonly fingerprint: NodeFingerprint; }; type SelectorCandidate = { readonly selector: string; readonly match_count: number | null; readonly matches_target: boolean; readonly error: string | null; readonly length: number; }; type XPathCandidate = { readonly xpath: string; readonly match_count: number | null; readonly first_match_is_target: boolean; readonly error: string | null; readonly length: number; }; type AccessibilityCandidate = { readonly role: string | null; readonly name: string; readonly description: string; readonly role_path: readonly AccessibilityPathSegment[]; readonly playwright_locator: string | null; readonly locator_match_count: number | null; readonly locator_matches_target: boolean | null; readonly inaccessible: boolean; }; type AccessibilityPathSegment = { readonly role: string; readonly name: string; readonly ordinal: number; }; type NodeFingerprint = { readonly tag_name: string; readonly id: string; readonly classes: readonly string[]; readonly attributes: Record; readonly text_hash: string; readonly child_element_count: number; }; type MutationRecipe = | "none" | "rename_classes" | "remove_ids" | "remove_testids" | "rename_accessible_names" | "remove_target"; type BenchmarkRun = { readonly name: string; readonly iterations: number; readonly target_count: number; readonly duration_ms: number; readonly consumed: number; readonly errors: number; }; const STABLE_ATTRIBUTE_NAMES = [ "data-testid", "data-test", "data-qa", "data-cy", "aria-label", "name", "title", "alt", "href", "src", ] as const; function cssEscape(value: string): string { const css = globalThis.CSS as { escape?: (value: string) => string } | undefined; if (typeof css?.escape === "function") return css.escape(value); return value.replace(/[^a-zA-Z0-9_-]/g, "\\$&"); } function quoteAttr(value: string): string { return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } function normalizeText(value: string): string { return value.replace(/\s+/g, " ").trim(); } function hashString(value: string): string { let hash = 2166136261; for (let index = 0; index < value.length; index += 1) { hash ^= value.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(16).padStart(8, "0"); } function rootFor(element: Element): Document | ShadowRoot { const root = element.getRootNode(); return root instanceof ShadowRoot ? root : element.ownerDocument; } function validateCssSelector( root: Document | ShadowRoot, target: Element, selector: string, ): SelectorCandidate { try { const matches = Array.from(root.querySelectorAll(selector)); return { selector, match_count: matches.length, matches_target: matches.includes(target), error: null, length: selector.length, }; } catch (error) { return { selector, match_count: null, matches_target: false, error: error instanceof Error ? error.message : String(error), length: selector.length, }; } } function stableTokens(element: Element): string[] { const tag = element.tagName.toLowerCase(); const tokens: string[] = []; const id = element.getAttribute("id"); if (id && !looksGenerated(id)) tokens.push(`#${cssEscape(id)}`); for (const name of STABLE_ATTRIBUTE_NAMES) { const value = element.getAttribute(name); if (!value) continue; if ((name === "href" || name === "src") && value.length > 80) continue; tokens.push(`${tag}[${name}="${quoteAttr(value)}"]`); tokens.push(`[${name}="${quoteAttr(value)}"]`); } const role = element.getAttribute("role"); if (role) tokens.push(`${tag}[role="${quoteAttr(role)}"]`, `[role="${quoteAttr(role)}"]`); for (const className of Array.from(element.classList)) { if (looksGenerated(className)) continue; tokens.push(`${tag}.${cssEscape(className)}`); } tokens.push(tag); return unique(tokens); } function looksGenerated(value: string): boolean { if (value.length > 36) return true; if (/css-[a-z0-9]{5,}/i.test(value)) return true; if (/^[_a-z]+-[a-z0-9_-]*__[a-z0-9_-]+$/i.test(value)) return true; if (/^[a-z]_[a-z0-9]{7,}$/i.test(value)) return true; if (/^[a-f0-9]{8,}$/i.test(value)) return true; return false; } function unique(values: readonly T[]): T[] { return Array.from(new Set(values)); } function customStableCssSelector(element: Element): string | null { const root = rootFor(element); let current: Element | null = element; const path: string[] = []; while (current) { const tokens = stableTokens(current); for (const token of tokens) { const candidate = [...path, token].reverse().join(" > "); const result = validateCssSelector(root, element, candidate); if (result.match_count === 1 && result.matches_target) return candidate; } const best = tokens[0] ?? current.tagName.toLowerCase(); path.push(best); current = current.parentElement; } return null; } function customAbsoluteCssSelector(element: Element): string { const segments: string[] = []; let current: Element | null = element; while (current) { const parent: Element | null = current.parentElement; const tag = current.tagName.toLowerCase(); if (!parent) { segments.unshift(tag); break; } const currentTag = current.tagName; const sameTagSiblings = Array.from(parent.children).filter((child): child is Element => child instanceof Element && child.tagName === currentTag ); const index = sameTagSiblings.indexOf(current) + 1; segments.unshift(`${tag}:nth-of-type(${index})`); current = parent; } return segments.join(" > "); } function buildAbsoluteXPath(element: Element): string { const segments: string[] = []; let current: Element | null = element; while (current) { const parent: Element | null = current.parentElement; const tag = current.tagName.toLowerCase(); if (!parent) { segments.unshift(tag); break; } const currentTag = current.tagName; const sameTagSiblings = Array.from(parent.children).filter((child): child is Element => child instanceof Element && child.tagName === currentTag ); const index = sameTagSiblings.indexOf(current) + 1; segments.unshift(`${tag}[${index}]`); current = parent; } return `/${segments.join("/")}`; } function buildStableXPath(element: Element): string | null { const tag = element.tagName.toLowerCase(); const id = element.getAttribute("id"); if (id && !looksGenerated(id)) return `//*[@id=${xpathLiteral(id)}]`; for (const attr of STABLE_ATTRIBUTE_NAMES) { const value = element.getAttribute(attr); if (!value) continue; return `//${tag}[@${attr}=${xpathLiteral(value)}]`; } const text = normalizeText(element.textContent ?? ""); if (text && text.length <= 80) return `//${tag}[normalize-space(.)=${xpathLiteral(text)}]`; return null; } function xpathLiteral(value: string): string { if (!value.includes('"')) return `"${value}"`; if (!value.includes("'")) return `'${value}'`; return `concat(${value.split('"').map((part) => `"${part}"`).join(', "\\"", ')})`; } function validateXPath(document: Document, target: Element, xpath: string): XPathCandidate { try { const snapshot = document.evaluate( xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null, ); const first = snapshot.snapshotLength > 0 ? snapshot.snapshotItem(0) : null; return { xpath, match_count: snapshot.snapshotLength, first_match_is_target: first === target, error: null, length: xpath.length, }; } catch (error) { return { xpath, match_count: null, first_match_is_target: false, error: error instanceof Error ? error.message : String(error), length: xpath.length, }; } } function accessibilityCandidate(element: Element): AccessibilityCandidate | null { const role = getRole(element); const name = computeAccessibleName(element); const description = computeAccessibleDescription(element); const path = buildAccessibilityPath(element); const playwrightLocator = role && name ? `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(name)} })${ ordinalSuffix(path) }` : null; const match = role && name ? countRoleNameMatches(element.ownerDocument, element, role, name) : null; return { role, name, description, role_path: path, playwright_locator: playwrightLocator, locator_match_count: match?.count ?? null, locator_matches_target: match?.matchesTarget ?? null, inaccessible: isInaccessible(element), }; } function ordinalSuffix(path: readonly AccessibilityPathSegment[]): string { const last = path.at(-1); if (!last || last.ordinal <= 1) return ""; return `.nth(${last.ordinal - 1})`; } function buildAccessibilityPath(element: Element): AccessibilityPathSegment[] { const segments: AccessibilityPathSegment[] = []; let current: Element | null = element; while (current) { const role = getRole(current); if (role) { const name = computeAccessibleName(current); segments.unshift({ role, name, ordinal: roleOrdinal(current, role, name) }); } current = current.parentElement; } return segments; } function roleOrdinal(element: Element, role: string, name: string): number { let ordinal = 0; const walker = element.ownerDocument.createTreeWalker( element.ownerDocument.body, NodeFilter.SHOW_ELEMENT, ); while (walker.nextNode()) { const candidate = walker.currentNode; if (!(candidate instanceof Element)) continue; if (getRole(candidate) !== role) continue; if (computeAccessibleName(candidate) !== name) continue; ordinal += 1; if (candidate === element) return ordinal; } return ordinal || 1; } function countRoleNameMatches( document: Document, target: Element, role: string, name: string, ): { count: number; matchesTarget: boolean } { let count = 0; let matchesTarget = false; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT); while (walker.nextNode()) { const candidate = walker.currentNode; if (!(candidate instanceof Element)) continue; if (getRole(candidate) !== role) continue; if (computeAccessibleName(candidate) !== name) continue; count += 1; if (candidate === target) matchesTarget = true; } return { count, matchesTarget }; } function fingerprint(element: Element): NodeFingerprint { const attributes: Record = {}; for (const attribute of Array.from(element.attributes)) { attributes[attribute.name] = attribute.value; } return { tag_name: element.tagName.toLowerCase(), id: element.id, classes: Array.from(element.classList), attributes, text_hash: hashString(normalizeText(element.textContent ?? "")), child_element_count: element.childElementCount, }; } function safeCssLibraryCandidate( name: string, element: Element, generator: () => string, ): SelectorCandidate | null { try { const selector = generator(); if (!selector) return null; return validateCssSelector(rootFor(element), element, selector); } catch (error) { return { selector: name, match_count: null, matches_target: false, error: error instanceof Error ? error.message : String(error), length: name.length, }; } } function targetByKey(targetKey: string): Element { const target = document.querySelector(`[data-probe-key="${quoteAttr(targetKey)}"]`); if (!target) throw new Error(`Target not found: ${targetKey}`); return target; } function probeTarget(targetKey: string): ProbeTargetResult { const target = targetByKey(targetKey); const root = rootFor(target); const customStable = customStableCssSelector(target); const customAbsolute = customAbsoluteCssSelector(target); const stableXpath = buildStableXPath(target); const absoluteXpath = buildAbsoluteXPath(target); const optimalSelect = typeof optimalSelectNamed === "function" ? optimalSelectNamed : optimalSelectDefault; return { target_key: targetKey, css: { custom_stable: customStable ? validateCssSelector(root, target, customStable) : null, custom_absolute: validateCssSelector(root, target, customAbsolute), finder: safeCssLibraryCandidate("finder", target, () => finder(target)), css_selector_generator: safeCssLibraryCandidate( "css-selector-generator", target, () => getCssSelector(target), ), optimal_select: safeCssLibraryCandidate( "optimal-select", target, () => optimalSelect(target), ), }, xpath: { stable: stableXpath ? validateXPath(target.ownerDocument, target, stableXpath) : null, absolute: validateXPath(target.ownerDocument, target, absoluteXpath), }, accessibility: accessibilityCandidate(target), fingerprint: fingerprint(target), }; } function applyMutation(recipe: MutationRecipe): void { if (recipe === "none") return; if (recipe === "rename_classes") { for (const element of Array.from(document.querySelectorAll("[class]"))) { element.setAttribute( "class", Array.from(element.classList).map((name) => `renamed-${name}`).join(" "), ); } return; } if (recipe === "remove_ids") { for (const element of Array.from(document.querySelectorAll("[id]"))) { element.removeAttribute("id"); } return; } if (recipe === "remove_testids") { for ( const element of Array.from( document.querySelectorAll("[data-testid], [data-test], [data-qa], [data-cy]"), ) ) { element.removeAttribute("data-testid"); element.removeAttribute("data-test"); element.removeAttribute("data-qa"); element.removeAttribute("data-cy"); } return; } if (recipe === "rename_accessible_names") { for (const element of Array.from(document.querySelectorAll("[aria-label]"))) { element.setAttribute( "aria-label", `Renamed ${element.getAttribute("aria-label") ?? ""}`.trim(), ); } for (const element of Array.from(document.querySelectorAll("button, a"))) { if (element.textContent) { element.textContent = `Renamed ${normalizeText(element.textContent)}`; } } return; } if (recipe === "remove_target") { document.querySelector("[data-probe-key]")?.remove(); } } function runBenchmark( name: string, targetKeys: readonly string[], iterations: number, ): BenchmarkRun { const startedAt = performance.now(); let consumed = 0; let errors = 0; for (let iteration = 0; iteration < iterations; iteration += 1) { for (const key of targetKeys) { const target = targetByKey(key); if (name === "custom_stable_css") consumed += customStableCssSelector(target)?.length ?? 0; else if (name === "custom_absolute_css") consumed += customAbsoluteCssSelector(target).length; else if (name === "finder") consumed += finder(target).length; else if (name === "css_selector_generator") consumed += getCssSelector(target).length; else if (name === "optimal_select") { try { const optimalSelect = typeof optimalSelectNamed === "function" ? optimalSelectNamed : optimalSelectDefault; consumed += String(optimalSelect(target)).length; } catch { errors += 1; } } else if (name === "absolute_xpath") consumed += buildAbsoluteXPath(target).length; else if (name === "accessibility") { consumed += accessibilityCandidate(target)?.playwright_locator?.length ?? 0; } } } return { name, iterations, target_count: targetKeys.length, duration_ms: performance.now() - startedAt, consumed, errors, }; } declare global { interface Window { __kaijuSelectorProbe: { probeTarget: typeof probeTarget; applyMutation: typeof applyMutation; runBenchmark: typeof runBenchmark; }; } } (globalThis as unknown as Window).__kaijuSelectorProbe = { probeTarget, applyMutation, runBenchmark, };