import { type Page, type Cookie as PWCookie } from "playwright"; import { type CookieJar } from "../cookies/index.js"; import { type AssertResult, type AssertSpec } from "./asserts.js"; import { type ContentAnnotationBox, type ContentChecksRequest, type ContentChecksResult } from "./content-checks.js"; import { type LayoutLintRequest, type LayoutLintResult } from "./geometry.js"; import { type OverflowResult, type WidthResult } from "./layout.js"; import { type RuntsResult } from "./runts.js"; import { type StandaloneHtmlResult } from "./standalone-html.js"; import { type TargetSizeProfile, type TargetSizeResult } from "./target-size.js"; import { type CheckVisibilityOptions, type VisibilityResult } from "./visibility.js"; /** * Headless-Chromium wrapper for the `browse` command. * * Two persistence layers: * 1. Persistent profile (Playwright's `launchPersistentContext`) keeps * browser state (localStorage, IndexedDB, login session) across runs. * 2. Optional cookie jar, shared with `fetch`/`cookies` so a session * built up in one tool is visible to the others. * * Designed to be opened, used for one or more navigations, and closed. * It is not a long-lived service. For multi-step workflows, the caller drives * `navigate`/`click`/`fill` directly between `open()` and `close()`. */ export interface BrowserOptions { /** Persistent profile dir. Default `~/.cache/harnery/browser-profile/`. Created if missing. */ profileDir?: string; /** Launch headed (visible window). Default false. */ headed?: boolean; /** Cookie jar to seed/sync with. Pass `null` to skip jar entirely. */ jar?: CookieJar | null; /** Viewport. Default 1280x800. */ viewport?: { width: number; height: number; }; /** * Device scale factor (DPR) for the context. Unset keeps Playwright's * default of 1, so screenshots are CSS-pixel sized. A higher value renders * every screenshot at that multiple (2 doubles both dimensions). */ deviceScaleFactor?: number; /** * Emulated `prefers-color-scheme` for the context. Unset keeps Playwright's * default (light), byte-identical to prior behavior. */ colorScheme?: "light" | "dark"; /** Default navigation timeout in ms. Default 30000. */ navigationTimeout?: number; /** * Max ms to wait for Chromium to start (`launchPersistentContext` timeout). * When unset, Playwright's default (30s) applies. Tests that need a faster * fail→retry loop should set this explicitly (e.g. 10_000). */ launchTimeout?: number; /** * `wait_until` strategy for `navigate`. Default `"load"`. * Use `"domcontentloaded"` for sites with long-running analytics scripts * that never let `"load"` fire. */ waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit"; /** * If set, record network traffic to a HAR file at this absolute path. * The HAR is finalized when `close()` is called. */ recordHarPath?: string; /** * Optional callback returning extra headers to attach to every request, * keyed by request URL. Consumers can inject extra HTTP headers per-URL * via this callback (e.g., a Cloudflare-bypass header for specific zones). */ extraHeaders?: (url: string) => Record; /** * Extra Chromium command-line flags, passed through to Playwright's * `launchPersistentContext` `args`. Used for environment-specific * workarounds — most notably `--disable-gpu` for headed windows under * WSLg (see `./launch-args.ts`). Empty/undefined means Playwright's * defaults only. */ launchArgs?: string[]; /** Authenticated browser proxy passed directly to Playwright. */ proxy?: { server: string; username?: string; password?: string; }; } export interface NavigateResult { url: string; title: string; status: number | null; } export interface ConsoleEvent { type: string; text: string; location?: { url: string; lineNumber?: number; columnNumber?: number; }; } export interface PageErrorEvent { message: string; stack?: string; } export interface FailedRequest { url: string; method: string; failure: string; resourceType: string; /** HTTP status for kind "http" entries; null for network-level failures. */ status: number | null; /** "http" = request completed with a >=400 response; "network" = never completed (DNS, TLS, aborts, tunnel). */ kind: "http" | "network"; /** True when the entry is the main frame's document response — lets consumers * distinguish an expected error-page status (a 404 route under test) from a * broken subresource. */ document?: boolean; } export interface Diagnostics { consoleEvents: ConsoleEvent[]; consoleErrors: ConsoleEvent[]; pageErrors: PageErrorEvent[]; failedRequests: FailedRequest[]; viewport: { width: number; height: number; } | null; } export type BrowserSessionLocator = { kind: "selector"; value: string; partial: boolean; } | { kind: "role"; value: string; name?: string; partial: boolean; } | { kind: "label"; value: string; partial: boolean; } | { kind: "text"; value: string; partial: boolean; }; export interface BrowserSessionStatus { phase: "ready"; active_tab: number; tab_count: number; revision: number; navigation: BrowserSessionNavigation; } export type BrowserSessionNavigationType = "navigate" | "reload" | "back_forward" | "prerender" | "unknown"; export interface BrowserSessionNavigation { /** Monotonic count of committed documents observed in this tab. */ sequence: number; /** UTC time when the latest document reached DOMContentLoaded. */ occurred_at: string | null; /** URL committed by the latest observed document navigation. */ url: string; /** Browser Navigation Timing classification for the current document. */ type: BrowserSessionNavigationType; } export interface BrowserSessionTab { index: number; title: string; url: string; active: boolean; revision: number; navigation: BrowserSessionNavigation; } export interface BrowserSessionControl { kind: string; name: string; attributes: Record; has_value?: boolean; } export interface BrowserSessionInspection { active_tab: number; url: string; title: string; text: string; controls: BrowserSessionControl[]; focus: BrowserSessionControl | null; revision: number; navigation: BrowserSessionNavigation; truncated: boolean; } export interface BrowserSessionScreenshot { path: string; width: number; height: number; revision: number; } export declare class BrowserSessionActionError extends Error { readonly code: string; constructor(code: string, message: string); } export declare class Browser { private opts; private context; private page; private pageIndexes; private pageRecency; private pageNavigations; private nextPageIndex; private recencyClock; private sessionRevision; readonly profileDir: string; private consoleEvents; private pageErrors; private failedRequests; constructor(opts?: BrowserOptions); /** Lazy: caller-side helper to find the active page if mid-flow. */ get currentPage(): Page; /** * Launch the context and wire up the first page. Factored out of `open()` so * the whole sequence — not just the launch — can be retried as a unit. */ private openOnce; /** * Open the browser, retrying a few times on a transient startup failure. * Chromium occasionally fails to hand off its CDP port ("Failed to connect", * "Target closed") when many instances launch at once — a busy CI box or a * full test suite — and the failure can surface on the launch OR on a * follow-up call (addCookies, newPage) when the process dies right after * spawning. So the entire open sequence retries as a unit, tearing down any * half-built context between attempts. A genuinely broken config fails every * attempt and throws with the underlying message. */ open(): Promise; /** * Hook console + pageerror + requestfailed events. Called on `open()` * before any navigation so we don't miss early-fired events. */ private attachDiagnosticListeners; private trackPage; private setActivePage; private recordDocumentNavigation; private describeNavigation; private selectMostRecentPage; /** * Snapshot of every event captured since `open()`. Returned objects are * copies, so callers can safely store them after `close()`. */ diagnostics(): Diagnostics; navigate(url: string): Promise; /** * Reload the current page. Preserves cookies + sessionStorage so callers can * reproduce sessionStorage-restored UI state (e.g. drawers/modals that open * automatically on reload, where Dialog auto-focus + Tooltip-on-focus may * interact differently than the click-to-open path). */ reload(): Promise; /** Full-page PNG screenshot. Returns the byte count written. */ screenshot(path: string, opts?: { fullPage?: boolean; }): Promise; /** Full document + viewport dimensions, for tiling a page into bands. */ pageMetrics(): Promise<{ scrollWidth: number; scrollHeight: number; viewportWidth: number; viewportHeight: number; }>; /** Full-page PNG as a Buffer, for cropping critique tiles in pixel space. */ fullPageScreenshotBuffer(): Promise; /** Wait for font-driven DOM/layout initialization, with a bounded convergence check. */ waitForReviewReady(timeoutMs?: number): Promise; /** Base64 PNG of a document-space clip rect. Used to capture one critique tile. */ screenshotClipBase64(rect: { x: number; y: number; width: number; height: number; }): Promise; /** * PNG of a document-space rect captured the way a reader sees it: scroll so * the rect's top sits at the top of the viewport, wait for the scroll to * settle (instant scroll, then rAF frames until `scrollY` stops moving), * and clip the viewport. Restores the scroll position afterwards. * * Bands are taller than the viewport. Rather than resizing the viewport to * the band height (which reflows `vh`-sized layout, moves sticky and fixed * elements, and so stops matching what the user sees), the rect is captured * in viewport-height pieces at the real viewport and the pieces are * stitched with pngjs. The final piece of a multi-piece rect is anchored to * the viewport bottom so a fixed header, which sits at the viewport top, is * not repeated in the middle of the band. No viewport change is made. * * Pixels come back at the page's device scale factor, so a rect at DPR 2 * yields an image twice the rect's CSS size, the same as a full-page * screenshot cropped in pixel space would. * * Parity with the full-page screenshot: a full-page capture renders the * whole document as one viewport, so a fixed header or a stuck sticky bar * appears once, at its document position. Scrolled, the same element would * ride along at the top of every piece and differ from the full-page crop * on every band but the first. So while a piece is captured at a non-zero * scroll, every `position: fixed`/`sticky` element that has left its * scroll-0 document position is given `visibility: hidden` (layout kept, * inline style restored afterwards). The result is the region as a * full-page screenshot would show it, without the full-page raster. */ captureRegionByScroll(rect: { x: number; y: number; width: number; height: number; }): Promise; /** Capture a document-ordered group with one pinned-element inventory and restore. */ captureRegionsByScroll(rects: Array<{ x: number; y: number; width: number; height: number; }>): Promise; /** Document-space rects + labels for each element matching `selector` (semantic tiling). */ elementTiles(selector: string): Promise>; /** * Extract the page's visual atoms for content-aware tiling (tiling.ts): * text line boxes (one per rendered line, so a cut between the lines of a * paragraph is legal), replaced/intrinsically-visual elements, and small * bordered boxes (cards, callouts — height-capped so section wrappers * don't count). Coordinates are document-space CSS px. */ visualAtoms(): Promise>; /** * Capture the page's QA signature for diff-aware classification * (qa-plan.ts): a structural fingerprint per element (path, tag, canonical * attributes, direct-text digest, nearest stable-ancestor anchor) plus * applied-stylesheet digests. DOM equality alone never proves a change is * text-only — a one-line edit to a linked stylesheet changes zero DOM bytes * and every pixel — so external sheet text is fetched and digested too; an * unreadable sheet records digest "unavailable" and the classifier widens. */ qaSignature(): Promise<{ nodes: Array<{ path: string; tag: string; attrs: string; text?: string; visual?: string; anchor?: { selector: string; path: string; }; }>; stylesheets: Array<{ key: string; kind: "external" | "inline"; digest: string; }>; domHtml: string; truncated: boolean; }>; /** * Capture a full-page screenshot from an explicit capture viewport and * evaluate the caller's final evidence expression immediately before the * pixels are written. Playwright normally manages the full-page viewport * internally, which leaves callers unable to inspect fixed/sticky geometry * in the state the PNG actually renders. */ screenshotWithEvaluation(path: string, evaluation: string, opts?: { fullPage?: boolean; }): Promise<{ bytes: number; evaluation: T; viewport: { width: number; height: number; }; evidence: { converged: boolean; reason: string; passes: number; max_passes: number; max_dimension: number; max_pixels: number; original_viewport: { width: number; height: number; }; evaluated_viewport: { width: number; height: number; }; document_extent_before_evaluation: { width: number; height: number; }; document_extent_after_evaluation: { width: number; height: number; }; document_extent_after_screenshot: { width: number; height: number; }; screenshot: { width: number; height: number; bytes: number; }; }; }>; /** * Plain-text snapshot of the document body. Suitable as a coarse "what's * on screen" signal for LLM iteration loops. For richer extraction, use * `htmlContent()` and pipe through a readability filter. */ textSnapshot(selector?: string): Promise; /** Raw outer HTML of the page (or a selector if provided). */ htmlContent(selector?: string): Promise; /** * Self-contained snapshot of the page: stylesheets inlined, fonts and images * embedded as data: URIs, everything else rewritten to absolute URLs on the * captured origin. Use this for anything written to a file — a raw * `htmlContent()` dump renders unstyled once it leaves the page's own origin. */ standaloneHtml(opts?: { maxResourceBytes?: number; maxTotalResourceBytes?: number; }): Promise; click(selector: string): Promise; fill(selector: string, value: string): Promise; press(key: string): Promise; waitForSelector(selector: string, timeout?: number): Promise; /** Evaluate JS in the page context. Caller is responsible for safety. */ evaluate(script: string): Promise; /** * Read the system clipboard via the page context. Grants `clipboard-read` * to the page's origin first because Chromium gates `navigator.clipboard * .readText()` behind a user-gesture + permission check; in headless * Playwright there is no user gesture, so the permission grant is the * substitute. Returns an empty string if the read returns nullish or * throws (insecure context, focus race). Used by `browse --batch * clipboard ...` to verify a UI Copy action end-to-end. */ readClipboard(): Promise; sessionStatus(): Promise; sessionTabs(): Promise; sessionSelectTab(index: number): Promise; sessionOpenTab(url: string): Promise; sessionCloseTab(index: number): Promise; sessionGoto(url: string): Promise; sessionReload(): Promise; sessionClick(locator: BrowserSessionLocator): Promise<{ revision: number; }>; sessionFill(locator: BrowserSessionLocator, value: string): Promise<{ revision: number; }>; sessionPress(key: string): Promise<{ revision: number; }>; sessionWait(locator: BrowserSessionLocator): Promise<{ revision: number; }>; sessionInspect(locator?: BrowserSessionLocator): Promise; sessionScreenshot(outInput: string): Promise; private sessionLocator; private strictSessionLocator; private requireStrictLocator; private pageForIndex; private describeTab; private selectMostRecentPageIfNeeded; /** * Run occlusion checks on one or more selectors. For each, samples a grid * of points inside the element's bounding rect and uses * `document.elementFromPoint` to detect whether the target is the topmost * paintable element at each sample. Catches the class of UI bugs where * an element's rect IS in-viewport but a higher-z-index sibling is * painting over it. */ checkVisibility(selectors: string[], opts?: CheckVisibilityOptions): Promise; /** Inject annotation overlays for visibility results. Used before screenshot. */ annotateVisibility(results: VisibilityResult[]): Promise; /** Remove visibility annotation overlays. */ clearVisibilityAnnotations(): Promise; /** * Measure each selector's bounding rect + viewport-fill + parent-fill * ratios. Catches the class of mobile-layout bug where a table sits at * (say) 85% viewport fill because of stacked padding: every per-element * check passes, but the user sees too-narrow content. */ checkWidth(selectors: string[]): Promise; /** * Detect horizontal overflow at the document level. Returns viewport size, * `document.scrollWidth`, and the top N elements protruding past the * viewport's right edge. Catches the class of bug where a nav/table is * wider than the viewport, forcing horizontal scroll on mobile. */ checkOverflow(opts?: { sampleLimit?: number; }): Promise; /** * Scan text blocks for runts — a single word alone on a block's last * visual line. Word-count per line via per-word Range rects (the width * of the last line is deliberately NOT the signal; see runts.ts). */ checkRunts(opts?: { scope?: string | null; minChars?: number; }): Promise; /** Inject annotation overlays for runt hits. Used before screenshot. */ annotateRunts(result: RuntsResult): Promise; /** Remove runt annotation overlays. */ clearRuntsAnnotations(): Promise; /** Inject annotation overlays for width + overflow results. Used before screenshot. */ annotateLayout(args: { widths: WidthResult[]; overflow: OverflowResult | null; widthThreshold: number; }): Promise; /** Remove layout annotation overlays. */ clearLayoutAnnotations(): Promise; /** Run the selector-scoped rendered-geometry rule family in one page evaluation. */ checkLayoutLint(request: LayoutLintRequest): Promise; /** Draw one document-space annotation layer for rendered-geometry results. */ annotateLayoutLint(result: LayoutLintResult): Promise; /** Remove rendered-geometry annotations. */ clearLayoutLintAnnotations(): Promise; /** Run the requested content checks (placeholder/image/truncation/contrast) in one evaluation. */ checkContent(request: ContentChecksRequest): Promise; /** Evaluate value assertions (text/contains/matches/count/exists/absent) against the page. */ checkAsserts(specs: AssertSpec[]): Promise; /** Draw one document-space annotation layer for content-check hits. */ annotateContent(boxes: ContentAnnotationBox[]): Promise; /** Remove content-check annotations. */ clearContentAnnotations(): Promise; /** * Run the target-size rule against the page or explicit scopes. The * dependency's browser bundle is evaluated directly in the page context so * a document CSP cannot silently block a script element. */ checkTargetSize(selectors: Array, profile: TargetSizeProfile): Promise; /** Draw target-size failures and unknowns on the screenshot. */ annotateTargetSize(results: TargetSizeResult[]): Promise; /** Remove target-size annotations. */ clearTargetSizeAnnotations(): Promise; /** * Inject a script that runs in every page context before page scripts * execute. Useful for seeding localStorage before an SSR/CSR comparison; * without this, state-dependent hydration mismatches are invisible to a * clean-profile probe. Must be called after `open()` and before * `navigate()`. */ addInitScript(script: string): Promise; /** Snapshot all cookies in the live persistent context. */ cookies(): Promise; /** * Sync cookies from the live context back into the jar (if one was provided), * then tear everything down. Safe to call multiple times. */ close(): Promise; } //# sourceMappingURL=client.d.ts.map