import type { Page } from '@playwright/test'; import { type CrawlConfidence } from './crawl-confidence.js'; /** Thrown when auth-boundary observation fails after a surface was recorded — must not yield false complete. */ export declare class AuthBoundaryObserveError extends Error { constructor(message: string, cause?: unknown); } /** * Surface crawler: deterministically map a single URL's WHOLE interactive * surface — not just the state it loads in — and run to natural termination. * * A one-shot capture records the page as it lands, so a design that's mostly * modals, drawers, and popovers has most of its surface behind clicks it never * reaches. This crawls it instead: from the base state it finds every visible, * non-destructive control (semantic controls AND anything the page styles as * clickable — `cursor: pointer`) and drives each one, keeping whatever opens a * structurally new surface. It then recurses depth-first into each new surface, * so a modal's tabs, a drawer's sub-views, and a popover's panels are all mapped. * * EXHAUSTIVE by default: the crawl stops when the queue drains and every control * has been driven once — not at a budget. Termination is guaranteed by dedup * (a control is driven once by selector; a structural surface is captured once; * both sets are finite). The `--max-*` options exist only as throttles. * * The sweep is IN-PLACE with lazy resets, which is what makes exhaustive * affordable: standing in a state, each candidate is clicked where the page * already is, and a cheap DOM fingerprint (tag+class in document order — no * computed styles) decides what happened. A no-op click costs nothing; only a * state-changing click pays a reset (fresh navigation + path replay), and every * reset is VERIFIED against the state's fingerprint before the sweep continues, * so children are never attributed to the wrong parent. New surfaces are * captured in place the moment they're reached — a deep or animated click-path * is never re-driven to capture, so it can't be the thing that drops a surface. * * Destructive-looking controls (delete, deploy, pay, revoke…) are never clicked — * mapping must not mutate. States gated behind such an action need a spec. */ export type CrawlStep = { action: 'click' | 'select-option' | 'fill-input'; selector: string; label: string; reason: string; value?: string; }; export type CrawledSurface = { key: string; depth: number; path: CrawlStep[]; elements: number; }; /** Did the crawl SEE everything the design styles? `missing` lists classes the * page's own stylesheets select on that never appeared in any captured surface — * dead CSS, or a state the crawl could not reach. `unreadable` names stylesheets * the browser could not parse (cross-origin, no CORS): their class vocabulary is * invisible, so coverage cannot be PROVEN against them. Full coverage = empty * `missing` AND empty `unreadable`. */ export type CrawlCoverage = { defined: number; rendered: number; missing: string[]; unreadable: string[]; /** The class names that WERE rendered — so a multi-page caller can aggregate * coverage across crawls sharing stylesheets (defined = rendered ∪ missing). */ renderedClasses: string[]; }; export type CrawlReport = { surfaces: CrawledSurface[]; actionsTried: number; skipped: number; /** Surfaces successfully captured to disk at every width. */ captured: number; /** Keys of surfaces discovered but whose full capture failed. */ failed: string[]; coverage: CrawlCoverage; /** * Run-level crawl confidence: `complete` | `incomplete-auth` | `incomplete-unknown`. * Auth walls yield redacted diagnostics only; unacknowledged walls set `blocked`. * Always present — consumers without auth walls see `status: 'complete'`. */ confidence: CrawlConfidence; }; /** * One deterministic step run after every fresh navigation, BEFORE the crawl * looks at the page — how input-gated states (a login form, a search box) * become crawlable. Values come from the caller with `${ENV_VAR}` interpolation * done at load time, so secrets live in the environment, never in files or maps. * `optional: true` skips the step silently when its selector isn't present * (e.g. a cookie-session app that only shows the login form once). */ export type SetupStep = { action: 'goto' | 'fill' | 'click' | 'waitFor'; url?: string; selector?: string; value?: string; optional?: boolean; }; export type SurfaceCrawlOptions = { url: string; out: string; widths: number[]; ignore: string[]; height: number; screenshots: boolean; waitSelector?: string; /** Deterministic steps (login, unlock, seed input) run after EVERY fresh * navigation, so each reset re-establishes the gated state identically. */ setup?: SetupStep[]; /** Also capture automatic data states of the entry page: `loading` (data * requests stalled — the skeleton) and `error` (data requests fulfilled with * a 500). Default true; states that render identically to base are skipped. */ dataStates?: boolean; /** Coverage-oriented termination: stop as soon as every class the page's * stylesheets define has been rendered (full coverage) OR coverage stops * improving (no new class for a plateau of surfaces — it has converged short * of full, and the rest is dead CSS or a genuinely-gated state). Off by * default (exhaustive: map every surface). On, the crawl is a fast coverage * check — it stops the moment it has SEEN everything, instead of enumerating * every combinatorial surface that adds no new vocabulary. */ stopWhenCovered?: boolean; /** Throttle: recursion depth into opened surfaces (base = 0). Default: unbounded. */ maxDepth: number; /** Throttle: fresh controls driven per state. Default: unbounded — try them all. */ maxActionsPerState: number; /** Throttle: total surfaces. Default: unbounded — run to natural termination. */ maxStates: number; /** Clear localStorage/sessionStorage on each reset so replay is deterministic. Default true. */ resetStorage: boolean; /** Called as each surface is recorded (captured=false when its full capture failed). * Lets CLIs stream progress instead of reporting only at the end. */ onSurface?: (surface: CrawledSurface, captured: boolean) => void; /** Concurrent sweep workers (default 4). Each worker gets its own page from * `newPage`; without a factory the crawl runs single-page regardless. The * surface SET is identical to a serial crawl (same dedup sets); only which * path first claims a shape — and so dup-key suffixes — can vary run to run. * Use workers: 1 for byte-stable key attribution. */ workers?: number; /** Factory for worker pages — create each in its OWN browser context so * storage resets cannot interfere across concurrent sweeps. */ newPage?: () => Promise; /** Namespace for every derived surface key (multi-page sweeps): the root * surface keys as the prefix itself, sub-states as `-