/** * What the lock file remembers about an element (docs/design/data-formats.md "Element descriptor"). The page builds it, * the runner stores and compares it, so the shape lives here where both can see it. */ export const ATTR_KEYS = ["aria-label", "data-testid", "id", "name", "type", "placeholder"] as const; export type AttrKey = (typeof ATTR_KEYS)[number]; export interface ElementDescriptor { tag: string; role: string; name: string; text: string; attrs: Record; classes: string[]; css: string; xpath: string; near: { heading: string | null; previousText: string | null; nextText: string | null }; rect: { x: number; y: number; w: number; h: number }; } const isRecord = (v: unknown): v is Record => typeof v === "object" && v !== null && !Array.isArray(v); const isString = (v: unknown): v is string => typeof v === "string"; const isStringOrNull = (v: unknown): v is string | null => v === null || typeof v === "string"; export function isElementDescriptor(v: unknown): v is ElementDescriptor { if (!isRecord(v)) return false; if (![v.tag, v.role, v.name, v.text, v.css, v.xpath].every(isString)) return false; if (!isRecord(v.attrs) || !ATTR_KEYS.every((k) => isStringOrNull((v.attrs as Record)[k]))) return false; if (!Array.isArray(v.classes) || !v.classes.every(isString)) return false; if (!isRecord(v.near) || !(["heading", "previousText", "nextText"] as const).every((k) => isStringOrNull((v.near as Record)[k]))) return false; return isRecord(v.rect) && (["x", "y", "w", "h"] as const).every((k) => Number.isFinite((v.rect as Record)[k])); } /** The same descriptor with its keys in one fixed order, so it is written the same way every time. */ export function normalizeDescriptor(d: ElementDescriptor): ElementDescriptor { return { tag: d.tag, role: d.role, name: d.name, text: d.text, attrs: Object.fromEntries(ATTR_KEYS.map((k) => [k, d.attrs[k]])) as ElementDescriptor["attrs"], classes: [...d.classes], css: d.css, xpath: d.xpath, near: { heading: d.near.heading, previousText: d.near.previousText, nextText: d.near.nextText }, rect: { x: d.rect.x, y: d.rect.y, w: d.rect.w, h: d.rect.h }, }; }