chrome

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 50 Imported by: 0

Documentation

Overview

Package chrome connects to Chrome over CDP (via chromedp) and exposes the Browser port the CLI commands drive. Keeping commands behind this interface lets the command boundary be tested in-process with a fake.

Index

Constants

View Source
const (
	// DefaultConsentTimeout is how long an open-but-silent endpoint is waited
	// out (config key consent_timeout). Two minutes is a human timescale for a
	// browser-modal dialog that can sit behind the window; ten seconds is not,
	// and ten seconds is what used to abandon the prompt it had just raised.
	DefaultConsentTimeout = 120 * time.Second
	// MinConsentTimeout and MaxConsentTimeout bound what a configured value is
	// allowed to be. Below the floor the "wait" is not a wait at all and the
	// prompt is abandoned as soon as it is raised — the original defect. Above
	// the ceiling it stops being a timeout: the daemon spawn lock is held for
	// as long as this value, so an inherited CHROME_CDP_CONSENT_TIMEOUT=8760h
	// would block every other invocation for a year.
	MinConsentTimeout = 1 * time.Second
	MaxConsentTimeout = 10 * time.Minute
	// DefaultConsentPendingAfter is how much silence from an open port counts
	// as "Chrome is asking the user". Two seconds, because this is loopback: a
	// debug endpoint that has accepted and then said nothing for two seconds is
	// not busy, it is waiting for a human.
	DefaultConsentPendingAfter = 2 * time.Second
)

Connection-probe timings. The dial and the pending threshold are short because this is loopback: a debug endpoint that has not accepted in two seconds is not slow, it is absent, and one that has accepted but said nothing for two seconds is not busy, it is waiting for a human.

View Source
const (
	// DefaultConsoleBuffer is the per-target ring size. 1000 lines is a few
	// hundred KB at the entry cap and covers the "what just broke" window that
	// motivates the verb, without retaining a day of framework chatter.
	DefaultConsoleBuffer = 1000

	// DefaultConsoleMaxEntry caps one message's text. A single console.log of a
	// megabyte-sized object is a real thing, and it must not be able to blow up
	// either the daemon's memory or the caller's context.
	DefaultConsoleMaxEntry = 8 << 10
)

Capture bounds. They are defaults, not policy: `console_buffer` and `console_max_entry` override them (see configureCapture), because the right size depends on how chatty the app under automation is.

View Source
const (
	DefaultFindLimit = 10
	MaxFindLimit     = 50
)

Find limits: the CLI validates --limit against these before connecting; the driver re-applies the default only for a zero value (a direct caller or the deliberately lenient daemon arg decoder).

View Source
const (
	// DefaultNetBuffer is the per-target ring size for network records. Half the
	// console's ring because a correlated request record — two header maps, a
	// URL, a request body — is an order of magnitude larger than a console line.
	DefaultNetBuffer = 500

	// DefaultNetMaxBody caps ONE body (request or response), in bytes. A JSON API
	// payload fits comfortably; a bundle or an image download must not be able to
	// blow up either the daemon's memory or the caller's context.
	DefaultNetMaxBody = 64 << 10
)

Network-capture bounds. They are defaults, not policy: `net_buffer` and `net_max_body` override them (see configureNetCapture).

View Source
const (
	// DefaultRecordFrames is the ring size. At the default scale and quality a
	// frame is a few tens of KB, so 600 frames is a couple of minutes of a
	// moving page for well under a hundred MB.
	DefaultRecordFrames = 600

	// DefaultRecordMaxBytes is the ceiling on retained frame bytes per tab. The
	// frame count alone does not bound memory — a 4K viewport frame is an order
	// of magnitude larger than a laptop one — so the byte ceiling is what makes
	// the worst case a constant rather than a function of the user's monitor.
	DefaultRecordMaxBytes = 96 << 20

	// DefaultRecordFPS is the capture cadence ceiling. GIFs rarely need more,
	// and every frame above it is memory spent on something nobody will see.
	DefaultRecordFPS = 4

	// DefaultRecordScale halves the captured dimensions, which quarters the
	// bytes for output that is still perfectly readable in an issue thread.
	DefaultRecordScale = 0.5

	// DefaultRecordQuality is the JPEG quality of a captured frame. High enough
	// that text stays legible, low enough that a long recording fits the cap.
	DefaultRecordQuality = 60

	// DefaultRecordMaxDuration stops a capture that was never stopped by hand.
	DefaultRecordMaxDuration = 2 * time.Minute
)

Capture defaults. They are defaults, not policy: the `record` flags override the per-recording ones and the `record_buffer` / `record_max_bytes` config keys override the daemon-wide bounds.

View Source
const DefaultArticleMinChars = 250

DefaultArticleMinChars is the `--min-chars` default: below this many extracted characters, `text --article` reports `extracted: false` and returns the full page text rather than a plausible-looking fragment.

View Source
const NetRedacted = "<redacted>"

NetRedacted is the placeholder a redacted header value or URL parameter is replaced with. It is part of the envelope contract: a caller can tell "the header was absent" from "the header was present and withheld".

Variables

View Source
var (
	// ErrAlreadyRecording reports a second `record start` on a tab that is
	// already recording. It is deliberately not a no-op: silently adopting the
	// existing recording would make `--fps 30` look like it took effect.
	ErrAlreadyRecording = errors.New("a recording is already active on this tab")

	// ErrNotRecording reports a stop/cancel with nothing to stop.
	ErrNotRecording = errors.New("no recording is active on this tab")
)

Recording lifecycle errors. They are sentinels because the CLI maps both to `usage` / exit 2, and — like every other sentinel here — they are matched through errIs, so they survive the daemon RPC flattening them to a string.

View Source
var (
	// ErrNotFileInput reports that the selector resolved to something that is
	// not an <input type=file>. The message names the element actually found.
	ErrNotFileInput = errors.New("element is not a file input")

	// ErrNotMultiple reports that more than one file was given to an input
	// without the `multiple` attribute. It is raised BEFORE dispatch, so the
	// input is left untouched.
	ErrNotMultiple = errors.New("input does not accept multiple files")

	// ErrAppendUnknown reports that --append was asked for on an input whose
	// existing files this session did not set, and therefore cannot re-send.
	ErrAppendUnknown = errors.New("--append cannot be honoured: this input's current files were not set by this session")
)

The upload failures that are the CALLER's bug rather than a timing problem. Each one happens AFTER the selector resolved, so retrying cannot help; the CLI maps them to usage / exit 2 so an agent can tell "fix your selector" from "wait longer" (RFC-0006 US-4).

View Source
var ConsoleLevels = []string{"debug", "log", "info", "warn", "error"}

ConsoleLevels are the level names `--level` accepts, in increasing severity. Exported so the CLI validates against exactly what the filter understands rather than a second, drifting copy of the list.

View Source
var ErrCoordinateOOB = errors.New("coordinate is outside the viewport")

ErrCoordinateOOB is a coordinate outside the current viewport.

It is an error rather than a clamp on purpose: the usual cause is a window that is not the size the caller thought, and clamping would convert a detectable mistake into a click on whatever happens to sit at the edge.

View Source
var ErrDropFailed = errors.New("the drop could not be delivered")

ErrDropFailed is a drop the page could not accept — the target went away, or the files did not reach the dispatch. It is the caller's address to fix, not a protocol fault, so the CLI classifies it as a target failure rather than letting it fall through to the generic cdp_error an agent reads as "retry".

View Source
var ErrNoHistory = errors.New("no history entry in that direction")

ErrNoHistory reports that a history navigation had no entry in the requested direction. Going nowhere is deliberately an error rather than a silent no-op: a wizard script that quietly failed to go back would carry on against the wrong page.

Its message is part of the contract: match it with IsNoHistory rather than errors.Is at call sites.

View Source
var ErrOccluded = errors.New("element has no settled, unoccluded clickable centre")

ErrOccluded reports that an element resolved but never presented an unoccluded centre — it is covered by an overlay, or it never stopped animating. Pointer verbs surface it as `occluded: true` in the error details, so a caller can tell "covered by an overlay" from "not found"; match it with IsOccluded rather than errors.Is at call sites.

View Source
var ErrZeroArea = errors.New("element resolved but has a zero-area box (display:none, or collapsed)")

ErrZeroArea reports that an element resolved but occupies no space — display:none, a collapsed container, or a zero-size image. There is nothing to capture, and a clip of zero width is a CDP error rather than an empty image, so this is raised before the capture. The CLI reports it as `zero_area: true`.

View Source
var NetTypes = []string{
	"document", "stylesheet", "image", "media", "font", "script", "texttrack",
	"xhr", "fetch", "prefetch", "eventsource", "websocket", "manifest",
	"signedexchange", "ping", "cspviolationreport", "preflight", "fedcm", "other",
}

NetTypes are the resource types `--type` accepts, matching Chrome's own vocabulary lowercased. Exported so the CLI validates against exactly what the filter understands rather than a second, drifting copy of the list.

Functions

func ClampConsentTimeout added in v0.2.0

func ClampConsentTimeout(d time.Duration) time.Duration

ClampConsentTimeout normalises a configured consent budget: zero or negative (unset, or a "0s" that meant nothing in particular) becomes the default, and anything outside [MinConsentTimeout, MaxConsentTimeout] is pulled to the nearer bound.

It exists so that every layer that reads this number reads it the SAME way. Before, chrome.Connect mapped <= 0 to the default, daemon.Ensure took the zero literally, and main forwarded the env var only when > 0 — so consent_timeout = "0s" produced a daemon waiting 120s, a client giving up at 10s, and the message "still waiting ... after 0s".

func FormatKeys added in v0.2.0

func FormatKeys(keys []KeyStroke) string

FormatKeys renders a sequence of strokes as a single keyspec.

func IsAlreadyRecording added in v0.2.0

func IsAlreadyRecording(err error) bool

IsAlreadyRecording reports whether err is ErrAlreadyRecording.

func IsCoordinateOOB added in v0.2.0

func IsCoordinateOOB(err error) bool

IsCoordinateOOB reports whether err is ErrCoordinateOOB, including after the daemon RPC has flattened it to a plain message.

func IsDropFailure added in v0.2.0

func IsDropFailure(err error) bool

IsDropFailure reports whether err is ErrDropFailed, including after the daemon RPC has flattened it to a plain message.

func IsNoHistory added in v0.2.0

func IsNoHistory(err error) bool

IsNoHistory reports whether err is ErrNoHistory.

func IsNotRecording added in v0.2.0

func IsNotRecording(err error) bool

IsNotRecording reports whether err is ErrNotRecording.

func IsOccluded added in v0.2.0

func IsOccluded(err error) bool

IsOccluded reports whether err is ErrOccluded.

func IsUploadUsage added in v0.2.0

func IsUploadUsage(err error) bool

IsUploadUsage reports whether err is one of the upload failures the caller must fix rather than retry. It matches the sentinel first and then its message, because an error raised inside the daemon reaches the CLI as a plain string and has lost its Go type — the same reason IsOccluded does.

func IsZeroArea added in v0.2.0

func IsZeroArea(err error) bool

IsZeroArea reports whether err is the zero-area condition. Like IsOccluded it falls back to a message match, because an error raised inside the daemon reaches the CLI as a plain string with its Go type gone — and the daemon is the default connection path.

func KeyNames added in v0.2.0

func KeyNames(keys []KeyStroke) []string

KeyNames returns the canonical name for every stroke, in order — the `keys` field of the `key` verb's result envelope.

func NetURLMatcher added in v0.2.0

func NetURLMatcher(spec string) (func(string) bool, error)

NetURLMatcher compiles a --url spec: a substring, or a regex behind a "re:" prefix. A nil matcher means "every URL".

func NormalizeConsoleLevel added in v0.2.0

func NormalizeConsoleLevel(s string) (string, bool)

NormalizeConsoleLevel maps a user- or Chrome-supplied level name onto the five documented levels, reporting whether it is one of them. Chrome's own vocabulary is wider than the CLI's (`warning`, `verbose`, `assert`, `trace`), so both ends go through here and a `--level warning` still means what the user obviously meant.

func NormalizeNetType added in v0.2.0

func NormalizeNetType(s string) (string, bool)

NormalizeNetType maps a user-supplied --type onto the documented vocabulary, reporting whether it is one of them. A few obvious aliases (css, img, ws) are accepted because they are what people type.

func PDFPageCount added in v0.2.0

func PDFPageCount(b []byte) int

PDFPageCount returns the number of pages in a PDF document, or 0 when the structure is not readable (an object-stream-compressed page tree, say) — the envelope reports what it can rather than failing a capture that succeeded.

func ParseModifiers added in v0.2.0

func ParseModifiers(s string) (int64, error)

ParseModifiers parses a `+`-joined modifier list ("cmd+shift") into the CDP modifier bitmask. An empty string is no modifiers, not an error.

It is deliberately independent of ParseKeys: the pointer verbs take a modifier list with no key attached (`hover --modifiers alt`), and sharing the parser is what keeps `cmd` meaning Meta in exactly one place.

func ProbeWS added in v0.2.0

func ProbeWS(wsURL string, wait time.Duration) browser.WSState

ProbeWS classifies an endpoint for a caller that wants the answer and not the socket — `doctor`, which must report what it verified and hold nothing.

It therefore does the thing Upgrade's doc comment warns about: it connects, learns the answer, and hangs up, spending the user's click on a connection nobody kept. That is the right trade HERE and only here. doctor is a diagnostic with nothing to hand a live socket to, and holding one open past the command that made it would be worse. What it must not do is pretend otherwise, so doctor's ready verdict says the connection was closed and the next command may prompt again — see runDoctor.

func RedactBody added in v0.2.0

func RedactBody(body string, noRedact bool) string

RedactBody withholds the values of credential-shaped fields in a request or response body, for the form-encoded and JSON shapes credentials actually travel in. noRedact returns the body unchanged.

RFC-0003 specifies redaction for headers and URLs only, so this goes beyond it deliberately. The premise of the whole tool is that it drives the user's real, logged-in browser: `net --body` on a login POST printed `password=…` in clear, while the SAME credential in `?password=…` was withheld — the same secret, opposite answers, decided by nothing but the HTTP method. US-5 asks for bodies that do "not spill tokens or PII into logs by default", and this is what that costs.

A body in any other encoding (multipart/form-data, protobuf, a bare token) is passed through: there is no field structure to key on, and guessing would either miss or mangle. `--body` remains an explicit opt-in either way.

func RedactHeaders added in v0.2.0

func RedactHeaders(h map[string]string, noRedact bool) map[string]string

RedactHeaders returns a copy of h with credential-shaped values replaced. noRedact returns the map unchanged, which is the ONLY way a live session token reaches the envelope.

func RedactURL added in v0.2.0

func RedactURL(raw string) string

RedactURL replaces the values of credential-shaped query and fragment parameters, preserving parameter order and encoding — re-encoding through url.Values would sort them and rewrite escapes, making the reported URL differ from the one the page actually requested.

func RedactedHeaderName added in v0.2.0

func RedactedHeaderName(name string) bool

RedactedHeaderName reports whether a header's value must be withheld: one of the named credential headers, or any name containing token/secret/password.

func RedactedParamName added in v0.2.0

func RedactedParamName(name string) bool

RedactedParamName reports whether a URL parameter or request/response body FIELD name is credential-shaped. One predicate for both, because a value is no less a secret for having travelled in a POST body than in a query string.

func ResolveWSURL added in v0.2.0

func ResolveWSURL(endpoint string) (string, bool)

ResolveWSURL returns the browser-level ws:// URL to probe and attach to.

A ws:// endpoint (the DevToolsActivePort path) is already one. An explicit --port names an http:// endpoint instead, and the browser-level WebSocket path is normally discoverable through Chrome's HTTP JSON API — the same resolution chromedp's remote allocator performs, done here first so the probe and the attach agree on what they are talking to.

This is NOT the consent check, and the difference matters: /json/version answers the same whether or not consent is pending (on the chrome://inspect path it 404s either way), so it can locate an endpoint and can never classify one. Only the upgrade does that.

Which is exactly why a failed lookup falls back to the ws:// ROOT of the same host:port rather than giving up. On the toggle path the JSON API is simply absent, so "no answer from /json/version" carried no information at all — and treating it as "no endpoint" made the pending state undetectable on the one path that actually prompts: `--port 9222` against a toggle-path Chrome holding an unanswered dialog reported not_debug_enabled, sending the user to re-enable a setting that was already on. Probing the root instead costs nothing in the granted case (an endpoint that will not upgrade there is classified refused, which is where it already was) and makes the hang — the one unambiguous consent signature — visible.

Types

type Browser

type Browser interface {
	List(ctx context.Context) ([]target.Info, error)
	Open(ctx context.Context, url string) (map[string]any, error)
	Navigate(ctx context.Context, targetID, url string) (map[string]any, error)
	// CloseTabs closes tabs by id. It is not Close, which tears down the whole
	// connection.
	CloseTabs(ctx context.Context, targetIDs []string) (map[string]any, error)
	// Activate foregrounds a tab within its window and raises that window, which
	// is the documented remedy for the accessibility-tree throttling that makes
	// --by name/ref/cell stall on a backgrounded tab (`tab_hidden`).
	Activate(ctx context.Context, targetID string) (map[string]any, error)
	// History navigates the tab by delta entries (-1 back, +1 forward). A delta
	// with no entry in that direction is an error, not a silent no-op: a wizard
	// script that quietly failed to go back would act against the wrong page.
	History(ctx context.Context, targetID string, delta int) (map[string]any, error)
	// Reload reloads the tab, optionally bypassing the cache.
	Reload(ctx context.Context, targetID string, hard bool) (map[string]any, error)
	Eval(ctx context.Context, targetID, expr string, opts EvalOpts) (any, error)
	Snapshot(ctx context.Context, targetID string, opts SnapOpts) (any, error)
	// Find ranks the page's accessibility nodes against a plain-language query
	// (RFC-0015) and returns the best matches with refs, states, and centre
	// points. It is a read: it never dispatches input, which is why MCP mode
	// can expose it under --read-only.
	Find(ctx context.Context, targetID, query string, opts FindOpts) (map[string]any, error)
	Key(ctx context.Context, targetID, selector string, keys []KeyStroke, opts KeyOpts) (map[string]any, error)
	// Pointer dispatches every pointer gesture — click included. There is no
	// separate Click: one method means one centre resolution and one place
	// modifiers are applied, which is what lets `click --modifiers cmd`
	// multi-select without a second implementation drifting from the first.
	Pointer(ctx context.Context, targetID, selector string, opts PointerOpts) (map[string]any, error)
	Select(ctx context.Context, targetID, field, option string, opts SelectOpts) (map[string]any, error)
	Grid(ctx context.Context, targetID, selector string, q QueryOpts) (any, error)
	Scroll(ctx context.Context, targetID, selector string, opts ScrollOpts) (map[string]any, error)
	Type(ctx context.Context, targetID, selector, text string, q QueryOpts) (map[string]any, error)
	Fill(ctx context.Context, targetID, selector, value string, q QueryOpts) (map[string]any, error)
	// Upload attaches local files to a file input. The paths must already be
	// absolute, existing, readable files — CDP requires absolute paths, and the
	// CLI validates them before connecting so a bad path never costs a
	// connection. The result reports the files READ BACK from the input.
	Upload(ctx context.Context, targetID, selector string, paths []string, opts UploadOpts) (map[string]any, error)
	HTML(ctx context.Context, targetID, selector string, inner bool, q QueryOpts) (map[string]any, error)
	Text(ctx context.Context, targetID, selector string, opts TextOpts) (map[string]any, error)
	Value(ctx context.Context, targetID, selector string, q QueryOpts) (map[string]any, error)
	Values(ctx context.Context, targetID, selector string, q QueryOpts) (map[string]any, error)
	AttrGet(ctx context.Context, targetID, selector, name string, q QueryOpts) (map[string]any, error)
	AttrList(ctx context.Context, targetID, selector string, q QueryOpts) (map[string]any, error)
	AttrSet(ctx context.Context, targetID, selector, name, value string, q QueryOpts) (map[string]any, error)
	AttrRemove(ctx context.Context, targetID, selector, name string, q QueryOpts) (map[string]any, error)
	SetHeaders(ctx context.Context, targetID string, headers map[string]string) (map[string]any, error)
	EmulateViewport(ctx context.Context, targetID string, width, height int64) (map[string]any, error)
	EmulateGeo(ctx context.Context, targetID string, lat, lon float64) (map[string]any, error)
	EmulateReset(ctx context.Context, targetID string) (map[string]any, error)
	// Window reports or sets the REAL Chrome window's bounds. It is not
	// EmulateViewport: that lies to the page about its size, while this moves
	// the window the user sees — which is what makes a screenshot's pixel
	// coordinates reproducible across runs (RFC-0014).
	Window(ctx context.Context, targetID string, opts WindowOpts) (WindowBounds, error)
	Frames(ctx context.Context, targetID string) (any, error)
	Wait(ctx context.Context, targetID string, cond WaitCond) (map[string]any, error)
	// Screenshot and PDF return the artifact bytes AND the metadata describing
	// them (dimensions, format, mode, resolved clip / page count). The metadata
	// travels with the bytes because only the driver can know it without the CLI
	// decoding the image itself.
	Screenshot(ctx context.Context, targetID string, opts ShotOpts) ([]byte, map[string]any, error)
	PDF(ctx context.Context, targetID string, opts PDFOpts) ([]byte, map[string]any, error)
	// RecordStart begins a screencast recording of a tab (RFC-0011). The frames
	// are retained where the CONNECTION lives — the daemon — because a recording
	// spans many CLI invocations and no per-command process can hold them. That
	// is also what makes a crashed automation's frames survive it (US-7).
	RecordStart(ctx context.Context, targetID string, opts RecordOpts) (map[string]any, error)
	// RecordStop ends the recording and hands back the retained frames plus the
	// accounting an honest export needs (frames, dropped_frames, truncated).
	// Encoding happens in the CLI, via internal/encode, so one capture can be
	// exported in more than one format and annotation stays an export decision.
	RecordStop(ctx context.Context, targetID string) ([]Frame, map[string]any, error)
	// RecordRestore hands a drained recording back, so an export that failed
	// after RecordStop can be retried instead of costing the user the frames.
	//
	// It exists because RecordStop is destructive and encoding is not free of
	// failure modes the CLI cannot rule out in advance (a full disk, an ffmpeg
	// that dies). Everything that CAN be checked first is — the encoder's
	// availability and the output path both are — and this covers the rest. The
	// restored recording is not capturing: it holds the frames for a retry, and
	// `record cancel` discards it like any other.
	RecordRestore(ctx context.Context, targetID string, frames []Frame, meta map[string]any) error
	// RecordStatus reports whether a recording is active and how much it holds.
	RecordStatus(ctx context.Context, targetID string) (map[string]any, error)
	// RecordCancel discards a recording without exporting anything.
	RecordCancel(ctx context.Context, targetID string) (map[string]any, error)
	// Console returns the console messages and uncaught exceptions retained for
	// a tab since the connection attached to it, filtered by opts BEFORE the
	// result is built (see ConsoleOpts in console.go).
	Console(ctx context.Context, targetID string, opts ConsoleOpts) (any, error)
	// ConsoleStream calls emit once per message as it arrives, until ctx ends.
	// It is the one Browser method that is not a single request/response, so
	// the daemon serves it over the streaming RPC path rather than dispatch.
	ConsoleStream(ctx context.Context, targetID string, opts ConsoleOpts, emit func(any) error) error
	// Net returns the HTTP requests retained for a tab since the connection
	// attached to it, filtered by opts BEFORE the result is built (see NetOpts
	// in net.go). Response bodies are fetched here, at read time, only when
	// opts.Body asks for them.
	Net(ctx context.Context, targetID string, opts NetOpts) (any, error)
	// NetStream calls emit once per COMPLETED request as it finishes, until ctx
	// ends. Like ConsoleStream it is served over the daemon's streaming RPC path
	// rather than dispatch.
	NetStream(ctx context.Context, targetID string, opts NetOpts, emit func(any) error) error
	// NetWait blocks until a request matching cond completes, answering from the
	// already-buffered records first so a request that landed between the action
	// and the wait is not missed.
	NetWait(ctx context.Context, targetID string, cond NetCond) (map[string]any, error)
	CookieList(ctx context.Context, targetID string) (any, error)
	CookieSet(ctx context.Context, targetID, name, value, domain, path string) (map[string]any, error)
	CookieDelete(ctx context.Context, targetID, name string) (map[string]any, error)
	CookieClear(ctx context.Context, targetID string) (map[string]any, error)
	Raw(ctx context.Context, targetID, method string, params json.RawMessage) (any, error)
	Close() error
}

Browser is the set of Chrome operations the CLI commands need. The real implementation is CDP (chromedp-backed); tests use a fake.

type CDP

type CDP struct {
	// contains filtered or unexported fields
}

CDP is the chromedp-backed Browser.

The allocator/base/tab contexts are rooted at context.Background(), NOT at a per-command context — otherwise a command's deferred cancel would tear the whole tree down and (in attach mode) CloseTarget the user's real tab. Tabs are attached once and reused; per-command timeouts are applied as short-lived child contexts around each action (see run). The shared daemon is the eventual home for this attach-and-hold model.

func Connect

func Connect(_ context.Context, opts Options) (*CDP, error)

Connect walks the connection ladder (mirroring browser.DecideConnection):

  • a completed WebSocket upgrade -> attach (Path B)
  • an open port with a hanging upgrade -> wait out Chrome's consent prompt, then ConnectError{consent_pending} if it is never answered
  • a running but non-debug Chrome -> ConnectError{not_debug_enabled} (never shadow the user's session with a second browser)
  • nothing running, --no-launch -> ConnectError{connection_failed}
  • nothing running -> launch a managed Chrome (Path A)

The ctx is intentionally not used to root the browser: the allocator lives at context.Background() so a command's deferred cancel can never tear down (and, in attach mode, CloseTarget) the user's real tabs. The decision (attach / instruct / launch) is delegated to the tested browser.DecideConnection so there is exactly one authored copy of the ladder.

func (*CDP) Activate added in v0.2.0

func (c *CDP) Activate(ctx context.Context, targetID string) (map[string]any, error)

Activate foregrounds a tab and raises its window — the documented remedy for the accessibility-tree throttling that makes --by name/ref/cell stall with `tab_hidden`.

It reports what actually happened rather than what was attempted:

  • was_active is sampled BEFORE acting, so a retry loop can tell "I fixed it" from "it was already foreground, so tab_hidden has another cause".
  • activated re-reads visibilityState afterwards instead of trusting the call, because bringToFront is silent about refusal.
  • window_focused is false when the OS or the (headless) build refused to raise the window. Overpromising would make the retry loop this verb exists to enable unreliable.

func (*CDP) AttrGet

func (c *CDP) AttrGet(ctx context.Context, id, selector, name string, q QueryOpts) (map[string]any, error)

func (*CDP) AttrList

func (c *CDP) AttrList(ctx context.Context, id, selector string, q QueryOpts) (map[string]any, error)

func (*CDP) AttrRemove

func (c *CDP) AttrRemove(ctx context.Context, id, selector, name string, q QueryOpts) (map[string]any, error)

func (*CDP) AttrSet

func (c *CDP) AttrSet(ctx context.Context, id, selector, name, value string, q QueryOpts) (map[string]any, error)

func (*CDP) Close

func (c *CDP) Close() error

Close tears down the connection. Managed Chrome is fully closed. An attached real Chrome is left alone — cancelling any context would CloseTarget a real tab, so we let the WebSocket drop with the process instead.

func (*CDP) CloseTabs added in v0.2.0

func (c *CDP) CloseTabs(ctx context.Context, targetIDs []string) (map[string]any, error)

CloseTabs closes each tab by id (Target.closeTarget at the browser level) and reports what went, so a caller that closed by filter can see exactly which tabs it destroyed. The url/title are read BEFORE closing — afterwards there is nothing left to describe.

A failure part-way through does not discard the tabs that did close: those are still reported, with the failures listed alongside. Only a close that achieved nothing is an error, so the caller sees `cdp_error` for "it didn't work" rather than for "it half worked".

func (*CDP) Console added in v0.2.0

func (c *CDP) Console(ctx context.Context, id string, opts ConsoleOpts) (any, error)

Console returns the buffered console messages for a tab, filtered server-side.

func (*CDP) ConsoleStream added in v0.2.0

func (c *CDP) ConsoleStream(ctx context.Context, id string, opts ConsoleOpts, emit func(any) error) error

ConsoleStream emits one payload per message as it arrives, until ctx ends. Reaching the caller's deadline is the normal way a --follow window closes, so it is a nil return, not an error.

func (*CDP) CookieClear

func (c *CDP) CookieClear(ctx context.Context, id string) (map[string]any, error)

func (*CDP) CookieDelete

func (c *CDP) CookieDelete(ctx context.Context, id, name string) (map[string]any, error)

func (*CDP) CookieList

func (c *CDP) CookieList(ctx context.Context, id string) (any, error)

func (*CDP) CookieSet

func (c *CDP) CookieSet(ctx context.Context, id, name, value, domain, path string) (map[string]any, error)

func (*CDP) EmulateGeo

func (c *CDP) EmulateGeo(ctx context.Context, id string, lat, lon float64) (map[string]any, error)

func (*CDP) EmulateReset

func (c *CDP) EmulateReset(ctx context.Context, id string) (map[string]any, error)

func (*CDP) EmulateViewport

func (c *CDP) EmulateViewport(ctx context.Context, id string, width, height int64) (map[string]any, error)

func (*CDP) Eval

func (c *CDP) Eval(ctx context.Context, id, expr string, opts EvalOpts) (any, error)

Eval evaluates expr in the tab and returns {"value": …}.

The DEFAULT path is untouched by RFC-0010 (Open Question 1): a plain expression evaluation in the page's main world. `--await` is opt-in because replMode changes how bare object literals and let/const re-declaration behave, and flipping that silently would break existing scripts.

func (*CDP) Fill

func (c *CDP) Fill(ctx context.Context, id, selector, value string, q QueryOpts) (map[string]any, error)

Fill sets a field to value, replacing (not appending to) any existing content: it triple-clicks the field to select all its text, then types value as real keystrokes over the selection — the reliable way to set a pre-filled cell (e.g. a timesheet "0" hour cell) to a new value in one call.

func (*CDP) Find added in v0.2.0

func (c *CDP) Find(ctx context.Context, id, query string, opts FindOpts) (map[string]any, error)

Find ranks the page's accessibility nodes against a plain-language query and returns the best matches with refs, states, and centre points.

func (*CDP) Frames

func (c *CDP) Frames(ctx context.Context, id string) (any, error)

Frames enumerates the tab's frame tree (Page.getFrameTree).

func (*CDP) Grid

func (c *CDP) Grid(ctx context.Context, id, selector string, q QueryOpts) (any, error)

Grid reads a table/grid as structured {headers, rows} from the accessibility tree — replacing hand-parsing the a11y snapshot (or a screenshot) for the calendar / task-list / timesheet grids. selector optionally picks the grid by ARIA accessible name (with q); empty selects the first grid/table in the tree.

func (*CDP) HTML

func (c *CDP) HTML(ctx context.Context, id, selector string, inner bool, q QueryOpts) (map[string]any, error)

func (*CDP) History added in v0.2.0

func (c *CDP) History(ctx context.Context, targetID string, delta int) (map[string]any, error)

History navigates the tab by delta entries (-1 back, +1 forward) and reports the settled URL. Unlike Reload it reports no `status`: a history move restored from the back/forward cache issues no request, so there is no HTTP response of its own to report — reporting the previous page's status would be a lie.

The entry is looked up with Page.getNavigationHistory BEFORE navigating, so "there is nothing in that direction" is a clean typed error instead of a navigation that never fires a load event and dies at the caller's timeout.

func (*CDP) Key added in v0.2.0

func (c *CDP) Key(ctx context.Context, id, selector string, keys []KeyStroke, opts KeyOpts) (map[string]any, error)

Key dispatches keyboard events that are not literal text — named keys, modifier chords, and repeats.

With a selector it resolves and focuses the element first, via the same occlusion-verified coordinate click Type uses, so it lands on a background tab where a box-model node click would poll until timeout. With an EMPTY selector it does no element resolution at all and dispatches to whatever the page currently has focused — which is the whole point of the verb for `key Escape` on a modal, or an arrow-key walk through a listbox, where the thing that must receive the key has no addressable selector.

func (*CDP) List

func (c *CDP) List(_ context.Context) ([]target.Info, error)

func (*CDP) Navigate

func (c *CDP) Navigate(ctx context.Context, id, url string) (map[string]any, error)

func (*CDP) Net added in v0.2.0

func (c *CDP) Net(ctx context.Context, id string, opts NetOpts) (any, error)

Net returns the buffered network records for a tab, filtered server-side.

func (*CDP) NetStream added in v0.2.0

func (c *CDP) NetStream(ctx context.Context, id string, opts NetOpts, emit func(any) error) error

NetStream emits one payload per COMPLETED request as it finishes, until ctx ends. Reaching the caller's deadline is the normal way a --follow window closes, so it is a nil return, not an error.

func (*CDP) NetWait added in v0.2.0

func (c *CDP) NetWait(ctx context.Context, id string, cond NetCond) (map[string]any, error)

NetWait blocks until a request matching cond completes.

It subscribes BEFORE scanning the buffer, then answers from the already- buffered records first. Both halves matter: scanning first would lose a request that completed between the scan and the subscription, and not scanning at all would time out on a request that completed between the action that triggered it and this call — the race a naive implementation loses (VS-10).

func (*CDP) Open

func (c *CDP) Open(ctx context.Context, url string) (map[string]any, error)

Open creates a new tab at url (browser-level Target.createTarget, which navigates it) and returns the new tab's id — replacing a raw Target.createTarget for the common "start from a fresh tab on X" case.

func (*CDP) PDF

func (c *CDP) PDF(ctx context.Context, id string, opts PDFOpts) ([]byte, map[string]any, error)

PDF prints the page to PDF (no chromedp Action; raw page.PrintToPDF).

func (*CDP) Pointer added in v0.2.0

func (c *CDP) Pointer(ctx context.Context, id, selector string, opts PointerOpts) (map[string]any, error)

Pointer dispatches one pointer gesture — click, hover, double-click, right-click, or drag — at an element's live, occlusion-verified centre.

All five verbs share one centre resolution (settledNodePoint) rather than recomputing geometry: a second box-model read would drift on overlapped elements and is simply wrong on a hidden tab. Modifiers are set on every dispatched event, so the page's handlers see event.metaKey / shiftKey and a modified click multi-selects the way a human's does.

func (*CDP) Raw

func (c *CDP) Raw(ctx context.Context, id, method string, params json.RawMessage) (any, error)

Raw sends any CDP method by string via the executor — full coverage, no per-method registry. An empty id targets the browser-level executor, so Browser.* / Target.* methods are reachable.

func (*CDP) RecordCancel added in v0.2.0

func (c *CDP) RecordCancel(ctx context.Context, id string) (map[string]any, error)

RecordCancel discards a recording.

func (*CDP) RecordRestore added in v0.2.0

func (c *CDP) RecordRestore(_ context.Context, id string, frames []Frame, meta map[string]any) error

RecordRestore re-seats a drained recording so a failed export can be retried.

RecordStop is destructive by design — the frames are handed over in full, so nothing is lost by the daemon forgetting them — but that is only true when the caller succeeds in writing them. Everything the CLI can check before draining it does check (the encoder's availability, the output path), and this covers what is left: a full disk, an ffmpeg that dies, a frame that will not encode. Without it, one transient failure ends the recording and the retry answers "no recording is active on this tab".

The restored recording is stopped: it holds frames for an export, it is not capturing. A recording that started on the tab meanwhile wins — clobbering a live capture to make room for a dead one would be the worse trade.

func (*CDP) RecordStart added in v0.2.0

func (c *CDP) RecordStart(ctx context.Context, id string, opts RecordOpts) (map[string]any, error)

RecordStart begins recording a tab.

func (*CDP) RecordStatus added in v0.2.0

func (c *CDP) RecordStatus(_ context.Context, id string) (map[string]any, error)

RecordStatus reports the live state of a recording.

func (*CDP) RecordStop added in v0.2.0

func (c *CDP) RecordStop(ctx context.Context, id string) ([]Frame, map[string]any, error)

RecordStop ends the recording and returns its frames.

The recording is removed whether or not the caller can encode what comes back: the frames are handed over in full, so nothing is lost by the daemon forgetting them. Whether the requested format can be produced at all is checked by the CLI BEFORE this call, which is what keeps a missing ffmpeg from costing the user their recording (VS-10).

func (*CDP) Reload added in v0.2.0

func (c *CDP) Reload(ctx context.Context, targetID string, hard bool) (map[string]any, error)

Reload reloads the tab, bypassing the cache when hard is set (the shift-reload the browser UI offers), and reports the settled URL.

func (*CDP) Screenshot

func (c *CDP) Screenshot(ctx context.Context, id string, opts ShotOpts) ([]byte, map[string]any, error)

Screenshot captures the tab and reports what it captured.

Four modes, in precedence order: an element's box (Selector), the whole scrollable page (FullPage), an explicit rectangle (Region), or — the default — the viewport. The CLI rejects more than one before connecting.

The returned metadata carries `mode` and the resolved `clip` because a capture that came out wrong is otherwise undebuggable: the image alone cannot tell you whether the wrong rectangle was chosen or the right one was rendered badly.

func (*CDP) Scroll

func (c *CDP) Scroll(ctx context.Context, id, selector string, opts ScrollOpts) (map[string]any, error)

Scroll scrolls a selector into view (Into), dispatches a real mouse wheel (Wheel), or — by default — scrolls by (Dx, Dy) via JS scrollBy on the selector's scroll box (or the window when selector is empty).

func (*CDP) Select

func (c *CDP) Select(ctx context.Context, id, field, option string, opts SelectOpts) (map[string]any, error)

Select chooses an option in a prompt / combobox / cascade widget, or a native <select>. It exists because a plain `click` cannot drive Workday's portal-rendered menus and React cascade prompts: those open on a real pointer sequence, mount briefly collapsed (a zero-scale transform) then animate open, render options in a detached subtree with delegated (capture-phase) handlers, and change geometry between the open and the option click. See .scratch/cdp-ergonomics/e3-rootcause.md.

The whole choreography runs inside ONE held CDP action (a single c.run), so geometry is re-read between steps against the live DOM — no multi-command staleness. Each interaction is a coordinate Input.dispatchMouseEvent (mouseMoved→pressed→released) at the element's live, occlusion-verified centre, which is what actually drives the delegated widgets where chromedp's node-click (one box-model read, no settle/occlusion check) misses.

func (*CDP) SetHeaders

func (c *CDP) SetHeaders(ctx context.Context, id string, headers map[string]string) (map[string]any, error)

func (*CDP) Snapshot

func (c *CDP) Snapshot(ctx context.Context, id string, opts SnapOpts) (any, error)

func (*CDP) Text

func (c *CDP) Text(ctx context.Context, id, selector string, opts TextOpts) (map[string]any, error)

Text returns the visible text of selector, or — with opts.Article — the page's main readable content with navigation, footers, and cookie banners dropped.

func (*CDP) Type

func (c *CDP) Type(ctx context.Context, id, selector, text string, q QueryOpts) (map[string]any, error)

Type coordinate-clicks the selector to focus it (robust on a background tab), then sends the text as real keystrokes to the focused element.

func (*CDP) Upload added in v0.2.0

func (c *CDP) Upload(ctx context.Context, id, selector string, paths []string, opts UploadOpts) (map[string]any, error)

Upload attaches local files to a file input via DOM.setFileInputFiles.

The paths must be absolute and already validated (the CLI does that before connecting). Everything this method checks is something only the page can answer: is the resolved element really a file input, does it accept several files, and what does it hold afterwards.

func (*CDP) Value

func (c *CDP) Value(ctx context.Context, id, selector string, q QueryOpts) (map[string]any, error)

func (*CDP) Values

func (c *CDP) Values(ctx context.Context, id, selector string, q QueryOpts) (map[string]any, error)

Values reads the value (or text) of EVERY element matching a CSS selector, in document order — one round trip instead of an eval per field (e.g. reading a whole row of timesheet hour cells or a set of selected pills). Uses querySelectorAll, so it works even on a background/hidden tab.

func (*CDP) Wait

func (c *CDP) Wait(ctx context.Context, id string, cond WaitCond) (map[string]any, error)

Wait blocks until a condition holds: the target URL contains cond.URL, or a selector becomes visible / is gone. The caller's context deadline bounds it.

func (*CDP) Window added in v0.2.0

func (c *CDP) Window(ctx context.Context, id string, opts WindowOpts) (WindowBounds, error)

Window reports the target's window bounds, or resizes it when opts carries dimensions.

Bounds are a BROWSER-domain concept keyed by window, so this runs through onBrowser — the executor Target.*/Browser.* calls need — rather than a page session, and names its target explicitly the way raiseWindow does.

type ConnectError

type ConnectError struct {
	Code    string
	Message string
}

ConnectError carries a stable error.code (matching the result contract) so the CLI can distinguish "not debug-enabled" from a generic connection failure.

func (*ConnectError) Error

func (e *ConnectError) Error() string

type ConsoleOpts added in v0.2.0

type ConsoleOpts struct {
	Grep   string        // keep only messages whose text matches this regex
	Levels []string      // keep only these levels (empty = all)
	Limit  int           // most recent N matches (0 = all)
	Since  time.Duration // only messages newer than this
	Clear  bool          // drop the buffer after reading
}

ConsoleOpts filters a console read. Every field is applied where the buffer lives — in the daemon, before the envelope is marshalled — so a chatty app cannot flood a caller's context (RFC-0002 US-5).

type EvalOpts added in v0.2.0

type EvalOpts struct {
	Await bool
}

EvalOpts controls the `eval` verb. Await switches Runtime.evaluate to REPL semantics — `awaitPromise` plus `replMode` — so a top-level `await` works and a statement list yields its final expression, exactly as DevTools' own console behaves. It is opt-in: replMode changes how bare object literals and `let`/`const` re-declaration are treated, so turning it on by default would silently alter existing scripts.

type FindOpts added in v0.2.0

type FindOpts struct {
	Role     string  // hard-filter matches to one ARIA role
	Limit    int     // maximum matches returned (default DefaultFindLimit)
	Region   string  // scope to the subtree of a container whose name contains this
	All      bool    // include ignored/hidden nodes (excluded by default)
	Dedupe   bool    // collapse identical role+name matches (keep first)
	MinScore float64 // drop matches scoring below this (0..1)
}

FindOpts controls the `find` verb.

type Frame added in v0.2.0

type Frame struct {
	Data   []byte    `json:"data"`
	TS     time.Time `json:"ts"`
	Width  int       `json:"width"`  // image pixels
	Height int       `json:"height"` // image pixels

	// CSSWidth/CSSHeight are what the frame covers in PAGE pixels. They are the
	// mapping a mark's coordinates need when the capture was scaled down, and
	// come from the screencast frame's own metadata rather than from the
	// requested scale — the two differ whenever the device scale factor is not 1.
	CSSWidth  float64 `json:"css_width,omitempty"`
	CSSHeight float64 `json:"css_height,omitempty"`

	// Marks are the actions that landed while this frame was on screen.
	Marks []FrameMark `json:"marks,omitempty"`
}

Frame is one captured screencast frame as it left the daemon.

Data is the JPEG exactly as Chrome produced it: the driver never re-encodes, so `record stop --format frames` hands back the capture's own pixels.

type FrameMark added in v0.2.0

type FrameMark struct {
	X       float64   `json:"x"`
	Y       float64   `json:"y"`
	Command string    `json:"command,omitempty"`
	TS      time.Time `json:"ts"`
}

FrameMark is one action that happened during a recording: where the pointer landed, in PAGE (CSS) pixels, and which verb put it there.

It is recorded alongside the frames and drawn — if at all — at export, which is what keeps capture free of any drawing (RFC-0011 design notes, VS-13). The coordinates come from the pointer verbs, which already resolve and report an occlusion-verified centre; nothing here re-resolves geometry.

type JSError added in v0.2.0

type JSError struct {
	Message string
	Stack   string
}

JSError is a JavaScript exception or promise rejection raised by an evaluated expression, kept distinct from a transport failure so the CLI can put the rejection's message and stack in the envelope's error details.

It exists because of the single most important correctness point in RFC-0010: an awaited promise that REJECTS must surface as an error, never as a successful envelope carrying an odd-looking value.

func JSException added in v0.2.0

func JSException(err error) (*JSError, bool)

JSException reports whether err is a JavaScript exception/rejection, and returns it. Callers use it to enrich the error envelope; everything else treats a JSError as an ordinary CDP error.

func (*JSError) Error added in v0.2.0

func (e *JSError) Error() string

type KeyOpts added in v0.2.0

type KeyOpts struct {
	Repeat int           // press the sequence this many times (1..100)
	Delay  time.Duration // pause between repeats, for apps that debounce
	Query  QueryOpts
}

KeyOpts controls the `key` verb — dispatching keyboard events that are not literal text (named keys, modifier chords, repeats). Query addresses an optional element to focus first; when the selector is empty the keys go to whatever the page currently has focused, which is what makes `key Escape` work when nothing is addressable.

type KeyStroke added in v0.2.0

type KeyStroke struct {
	Key       string // DOM KeyboardEvent.key ("Escape", "a", "ArrowDown")
	Code      string // DOM KeyboardEvent.code ("Escape", "KeyA", "ArrowDown")
	KeyCode   int64  // windowsVirtualKeyCode
	Text      string // the text a printable key inserts ("" for non-printable)
	Modifiers int64  // CDP modifier bitmask (alt 1, ctrl 2, meta 4, shift 8)
}

KeyStroke is one resolved keyboard press: the key to dispatch plus the modifier bitmask held during it. It is produced by ParseKeys from a `key` verb argument and carries the DOM `key`/`code`/virtual-keycode tuple, because frameworks read all three and a page that checks `event.code` ignores a press that only sets `event.key`.

func ParseKeys added in v0.2.0

func ParseKeys(spec string) ([]KeyStroke, error)

ParseKeys parses a keyspec into the strokes to dispatch. It is pure: no context, no browser, no clock — so the CLI can validate the argument before it connects to Chrome, and so the grammar is testable without a renderer.

Accepted forms, which may be combined into a space-separated sequence:

Escape        a named key (matched case-insensitively)
a  /  7       one printable character
cmd+a         a chord: modifiers, then exactly one key
"End shift+Home Backspace"   a sequence, pressed left to right

func (KeyStroke) String added in v0.2.0

func (k KeyStroke) String() string

String formats a stroke back into the keyspec syntax that parses to it, so the result envelope can echo what was pressed in the same language the caller used.

type NetCond added in v0.2.0

type NetCond struct {
	URL     string
	Methods []string
	Status  string
	Types   []string
	Failed  bool

	Headers  bool
	Body     bool
	NoRedact bool
}

NetCond is the request `wait --request` / `net wait` blocks for. It is the filter half of NetOpts plus the rendering flags for the one record it returns — there is no --limit or --clear, because it answers about a single request.

type NetOpts added in v0.2.0

type NetOpts struct {
	URL     string        // substring match; a "re:" prefix switches to regex
	Methods []string      // keep only these HTTP methods (empty = all)
	Status  string        // --status spec: 200 | 2xx | >=400 | !2xx (empty = all)
	Types   []string      // keep only these resource types (empty = all)
	Failed  bool          // keep only non-2xx or network-level failures
	Limit   int           // most recent N matches (0 = all)
	Since   time.Duration // only requests newer than this
	Clear   bool          // drop the buffer after reading

	// Rendering. Headers and bodies are ABSENT from the envelope unless asked
	// for; NoRedact turns off the credential redaction that is otherwise always
	// applied to headers and URLs.
	Headers  bool
	Body     bool
	NoRedact bool
}

NetOpts filters a network read. Every field is applied where the buffer lives — in the daemon, before the envelope is marshalled — so a chatty page cannot flood a caller's context.

type NetStatusMatcher added in v0.2.0

type NetStatusMatcher func(status int64, hasStatus bool) bool

NetStatusMatcher reports whether an HTTP status satisfies a --status spec.

A record with NO status — a network-level failure, or a request still in flight — never matches ANY spec, including a negated one. "not 2xx" would otherwise silently include every pending request, which is the opposite of what somebody asserting on an outcome means.

func ParseNetStatus added in v0.2.0

func ParseNetStatus(spec string) (NetStatusMatcher, error)

ParseNetStatus compiles a --status spec: an exact code (200), a class (2xx), a comparison (>=400, !=204), or a negation of any of those (!2xx).

It is pure and runs in the CLI BEFORE anything connects, so a malformed spec is usage/exit 2 with Chrome never contacted.

type Options

type Options struct {
	PortFile   string // override DevToolsActivePort location (else OS candidates)
	ProfileDir string // managed-launch profile dir (else CHROME_CDP_PROFILE / default)
	Port       int    // explicit debug port to attach to / launch with (0 = auto)
	NoLaunch   bool   // don't fall back to launching a managed Chrome
	Headless   bool   // headless for the managed-launch fallback (tests use this)

	// ConsentTimeout bounds the wait for Chrome's "Allow remote debugging?"
	// dialog (config key consent_timeout). It applies ONLY to an open port
	// whose upgrade is hanging; a refused endpoint still fails in
	// milliseconds. See browser.AwaitUpgrade.
	//
	// It arrives already normalised: config resolution is the one boundary
	// where flag, env and file meet, and ClampConsentTimeout runs there.
	ConsentTimeout time.Duration
	// ConsentPendingAfter is how much silence from an OPEN port counts as
	// "Chrome is asking the user", i.e. when OnConsentPending fires. Zero means
	// DefaultConsentPendingAfter, which is what production always uses.
	//
	// It is a field rather than a package var because a test needs to shrink
	// it, and the tests that need it most are in another package: the daemon's
	// had to sit a 3s consent budget and a 3s poll around a 2s threshold it
	// could not reach, measuring 3.05s against a marker at ~2.0s. A 1.5x margin
	// on a timing this repo has already been bitten by four times on macOS is
	// not a margin. Options already carries ConsentTimeout and
	// OnConsentPending; this belongs with them.
	ConsentPendingAfter time.Duration
	// OnConsentPending fires once, as soon as the upgrade is classified as
	// pending — i.e. while the dialog is still on screen, not after the wait.
	// The daemon uses it to tell the CLI what it is waiting for.
	OnConsentPending func()

	// Event-capture bounds (config keys console_buffer / console_max_entry).
	// Zero means the built-in default; see configureCapture.
	ConsoleBuffer   int // retained console messages per target
	ConsoleMaxEntry int // per-message text cap, in bytes

	// Network-capture bounds (config keys net_buffer / net_max_body). Separate
	// from the console's because a correlated request record is much larger than
	// a console line. Zero means the built-in default.
	NetBuffer  int // retained network records per target
	NetMaxBody int // per-body cap, in bytes

	// Recording bounds (config keys record_buffer / record_max_bytes). A frame
	// is orders of magnitude larger than either of the above, so it gets its own
	// ring size AND a byte ceiling — a frame count alone does not bound memory
	// when the viewport can be 4K. Zero means the built-in default.
	RecordBuffer   int // retained frames per target
	RecordMaxBytes int // retained frame bytes per target
}

Options controls how CDP connects.

type PDFOpts added in v0.2.0

type PDFOpts struct {
	Landscape    bool
	PaperWidth   float64 // inches; 0 = Chrome's default (letter)
	PaperHeight  float64 // inches; 0 = Chrome's default (letter)
	MarginTop    float64 // inches
	MarginRight  float64 // inches
	MarginBottom float64 // inches
	MarginLeft   float64 // inches
	Scale        float64 // 0.1–2 (0 means 1)
	Background   bool    // print background graphics
	Pages        string  // page ranges, e.g. "1-3,5" ("" = the whole document)
	Header       string  // HTML template for the print header
	Footer       string  // HTML template for the print footer
}

PDFOpts controls the `pdf` verb. Every user-facing spelling — named paper sizes, `WxH` inches, one-or-four-value margins, page ranges — is parsed in the CLI, so the driver receives numbers only and has no grammar of its own.

Lengths are inches, matching Page.printToPDF. Zero margins are meaningful (the CLI always resolves a value, defaulting to 0.4in), while a zero paper size means "leave Chrome's default".

type Point added in v0.2.0

type Point struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
}

Point is a viewport coordinate in CSS pixels: origin at the top-left of the layout viewport, x right, y down.

This is the same space snap geometry, the pointer verbs' centre resolution, and Input.dispatchMouseEvent already use, and the space a `screenshot --scale 1` capture maps onto 1:1 — which is what makes "look at a screenshot, act at a coordinate" work (RFC-0014).

type PointerAction added in v0.2.0

type PointerAction string

PointerAction names a pointer verb.

const (
	PointerClick    PointerAction = "click"
	PointerHover    PointerAction = "hover"
	PointerDblClick PointerAction = "dblclick"
	PointerRClick   PointerAction = "rclick"
	PointerDrag     PointerAction = "drag"
	// PointerTripleClick selects a text block — the standard prelude to copying
	// or overwriting it. `fill` performs the same gesture internally; this
	// exposes it for a caller that wants only the selection.
	PointerTripleClick PointerAction = "tripleclick"
)

The pointer actions a single Pointer call can dispatch.

type PointerOpts added in v0.2.0

type PointerOpts struct {
	Action    PointerAction
	Modifiers int64 // held during the press (alt 1, ctrl 2, meta 4, shift 8)

	// At bypasses element resolution: the gesture is dispatched at this viewport
	// coordinate. It is how a canvas, a map, or any surface the accessibility
	// tree cannot see gets driven — and how a screenshot-reading agent acts on
	// what it saw. When set, no selector is resolved and no occlusion check
	// runs: the coordinate IS the intent (RFC-0014).
	At *Point

	// Drag targeting: a To selector (resolved with ToQuery), a ToAt coordinate,
	// or a (Dx, Dy) pixel delta from the source. Exactly one is set.
	To      string
	ToQuery QueryOpts
	ToAt    *Point
	Dx, Dy  float64

	// Steps is the number of interpolated move events dispatched between the
	// press and the release. It is not cosmetic: drag implementations require
	// movement to register a drag at all, and a press-then-release at two points
	// is silently a click.
	Steps int
	Hold  time.Duration // pause after press before moving (long-press-to-drag UIs)

	Query QueryOpts
}

PointerOpts controls the pointer verbs — click, hover, dblclick, rclick, and drag. One driver method backs all five, so they share the identical occlusion-verified centre resolution and the identical modifier handling; Action selects which gesture is dispatched.

type QueryOpts

type QueryOpts struct {
	By     string // css (default) | id | search | jspath | css-all | name
	Wait   string // visible (default) | ready | enabled
	Pierce bool   // reach into shadow DOM / iframes (via DevTools search)

	// Accessible-name addressing (By == "name"): the selector is the ARIA
	// accessible name; Role optionally constrains the ARIA role, and Nth picks
	// the 1-based match among the visible candidates (0 = first).
	Role string
	Nth  int

	// Match controls how the accessible name is compared (By == "name"):
	// "" / "exact" (default), "contains" (case-insensitive substring), or
	// "regex". Lets a caller click "Review" without knowing its verbose full
	// accessible name.
	Match string

	// InRow scopes an accessible-name match (By == "name") to the table row
	// whose text contains this string — so `click --by name "Delete" --in-row
	// "TEST entry"` hits the Delete button in that row, not the first of many.
	// It resolves via the DOM (not the throttled a11y tree), so it also works on
	// a backgrounded tab. Requires By == "name".
	InRow string

	// OnDialog, set on an action verb (click/type/fill), auto-handles a native
	// JavaScript dialog (alert/confirm/prompt) that opens DURING the action —
	// "accept" or "dismiss" — instead of letting it wedge the CDP connection.
	// The handled dialogs are reported back in the result envelope.
	OnDialog string
}

QueryOpts controls how a selector is interpreted (By), what state to wait for (Wait), and whether to pierce shadow DOM / iframes (Pierce).

type RecordOpts added in v0.2.0

type RecordOpts struct {
	// FPS is the CAPTURE cadence ceiling, not a fixed interval: a screencast
	// pushes frames only when the page changes, so this throttles a busy page
	// rather than polling a static one.
	FPS float64

	// Scale shrinks the captured frames relative to the viewport. It is applied
	// by Chrome at capture time (maxWidth/maxHeight), so a scaled recording
	// costs less memory in the daemon, not just less disk at export.
	Scale float64

	// Quality is the JPEG quality of each frame, 0-100.
	Quality int

	// MaxFrames is the ring-buffer bound. Eviction is REPORTED (dropped_frames /
	// truncated), never silent.
	MaxFrames int

	// MaxDuration stops the capture — but not the recording — after this long.
	// The frames captured so far stay exportable, with truncated set.
	MaxDuration time.Duration

	// Annotate is the DEFAULT for the export, not a capture-time behaviour.
	// Action marks are recorded either way (they are a few floats each), so one
	// capture can be exported both annotated and clean; `record stop` may
	// override this.
	Annotate bool
}

RecordOpts controls `record start` — the screencast capture behind RFC-0011.

Every value is validated and reduced to a number by the CLI before it arrives here; the driver applies its own defaults only for a zero field, which is what a direct (non-CLI) caller and the deliberately lenient daemon arg decoders leave behind.

type Rect added in v0.2.0

type Rect struct {
	X      float64 `json:"x"`
	Y      float64 `json:"y"`
	Width  float64 `json:"width"`
	Height float64 `json:"height"`
}

Rect is a rectangle in PAGE (document) coordinates, in CSS pixels — the same space Page.captureScreenshot's clip uses, so it is unaffected by scrolling. It is reported back as the envelope's `clip`: the single most useful field for debugging a capture that came out wrong.

type ScrollOpts

type ScrollOpts struct {
	Dx    float64
	Dy    float64
	Into  bool
	Wheel bool

	// At anchors a --wheel dispatch at a viewport coordinate instead of an
	// element's centre — for maps and canvases that zoom around the pointer.
	At *Point

	Query QueryOpts
}

ScrollOpts controls the `scroll` verb:

  • Into: scroll Selector into view.
  • otherwise: scroll by (Dx, Dy) pixels — Selector's scroll box, or the window when Selector is empty. This is a JS scrollBy (deterministic; it fires the scroll events virtualized grids render on).
  • Wheel: instead dispatch a real mouse-wheel of (Dx, Dy) at Selector's centre (viewport centre if empty), for grids that listen for wheel specifically.

type SelectOpts

type SelectOpts struct {
	// Query addresses the FIELD control (usually By=="name" with a Role like
	// textbox/combobox to disambiguate a same-named column header from the input).
	Query QueryOpts

	// OptionMatch controls how each option segment is compared against rendered
	// option text: "" / "contains" (default — Workday labels are verbose),
	// "exact", or "regex".
	OptionMatch string

	// Filter is optional text typed into the prompt after it opens, to narrow a
	// long option list before the option is clicked.
	Filter string

	// Sep splits Option into cascade levels (default ">"): e.g.
	// "Project Plan Tasks > ShiftLeft: Qwiet" drills the category, then the leaf.
	Sep string
}

SelectOpts controls the `select` verb — choosing an option in a prompt / combobox / cascade widget (Workday-style portal popups, or a native <select>).

type ShotMode added in v0.2.0

type ShotMode string

ShotMode names which capture path a screenshot took. It is reported in the envelope so a caller can confirm which of the four ran without inspecting the image.

const (
	ShotViewport ShotMode = "viewport"
	ShotElement  ShotMode = "element"
	ShotFullPage ShotMode = "full_page"
	ShotRegion   ShotMode = "region"
)

The capture modes a single Screenshot call can take.

type ShotOpts added in v0.2.0

type ShotOpts struct {
	Selector string // capture this element's box (all QueryOpts flags apply)
	FullPage bool   // capture the whole scrollable page, beyond the fold
	Region   *Rect  // capture an explicit page-coordinate rectangle

	Format  string  // png (default) | jpeg | webp
	Quality int     // 0–100; jpeg/webp only (ignored for png, which the CLI rejects)
	Scale   float64 // output scale factor, 0.1–3 (0 means 1)
	Padding float64 // expand an element clip by this many px, clamped to the page

	Query QueryOpts
}

ShotOpts controls the `screenshot` verb. Selector, FullPage, and Region select the capture mode and are mutually exclusive — the CLI rejects more than one before connecting; the driver treats them in that precedence order.

Every value here is already validated and normalized by the CLI: Format is a known enum, Quality is in range and only set for a lossy format, Scale is within 0.1–3. The driver does not re-check user input.

type SnapOpts

type SnapOpts struct {
	Role   string // only nodes with this ARIA role
	Grep   string // only nodes whose accessible name matches this regex
	Region string // only nodes within the subtree of a container whose name contains this
	Dedupe bool   // collapse identical role+name (keep first) — for virtualized grids
}

SnapOpts filters an accessibility snapshot server-side, so a read returns just the relevant nodes instead of the whole tree. Alerts/focused stay page-wide.

type TextOpts added in v0.2.0

type TextOpts struct {
	Article  bool // extract the main readable content, dropping boilerplate
	Markdown bool // with Article: emit markdown structure instead of plain text
	MinChars int  // with Article: below this many extracted chars, report failure
	Query    QueryOpts
}

TextOpts controls the `text` verb. The zero value is the long-standing behaviour: the visible text of Query's selector, verbatim.

Article turns on Readability-style main-content extraction, which is a heuristic — so MinChars is the honesty threshold below which the extraction is reported as failed (`extracted: false`) and the FULL page text is returned instead of a plausible-looking fragment. Markdown preserves headings, lists, links, code blocks, and blockquotes; it is deliberately not a general HTML-to-markdown converter.

type Upgrade added in v0.2.0

type Upgrade struct {
	State browser.WSState
	// contains filtered or unexported fields
}

Upgrade is one probe's outcome plus, when the endpoint accepted, the socket it used.

The socket is kept rather than closed on purpose. Chrome asks for consent per fresh attach, so a probe that connects, learns the answer and hangs up has spent the user's click on a connection nobody kept; the follow-on attach would be a fresh one again. Holding it open until the real attach has been established means the click the user just made is still doing work when the attach lands. Close it as soon as the attach returns — see chrome.Connect.

func AwaitUpgrade added in v0.2.0

func AwaitUpgrade(wsURL string, t UpgradeTimings, onPending func()) *Upgrade

AwaitUpgrade dials wsURL and performs exactly ONE WebSocket handshake against it, classifying the result.

It is deliberately a single connection: every connection to the debug endpoint is a consent request, and stacking those is what wedges a browser.

func (*Upgrade) Close added in v0.2.0

func (u *Upgrade) Close()

Close releases the probe socket. Safe on a refused or pending Upgrade, which have no socket to release — AwaitUpgrade never returns nil, so that is the only case there is.

type UpgradeTimings added in v0.2.0

type UpgradeTimings struct {
	// PendingAfter is how much silence counts as "Chrome is asking the user".
	// Reaching it calls onPending (once) so the caller can say so WHILE it
	// waits, rather than afterwards, which would be a post-mortem.
	PendingAfter time.Duration
	// Total is the whole budget. The same upgrade stays open across it, so an
	// answer that arrives late still lands on a live connection instead of an
	// orphaned one.
	Total time.Duration
}

UpgradeTimings bounds one probe. It is a struct rather than two positional durations because the two are easy to swap and the consequences are not symmetric: a pending threshold longer than the budget announces nothing, and a budget shorter than the threshold abandons the prompt it just raised.

type UploadOpts added in v0.2.0

type UploadOpts struct {
	// Append adds to the files this session already set on the input instead of
	// replacing them. It can only be honoured for files this CLI set itself:
	// setFileInputFiles replaces the FileList wholesale, and the existing
	// entries' PATHS are not readable back from the DOM (File.name is the bare
	// name by design). Appending to an input whose prior contents are unknown is
	// a usage error rather than a guess.
	Append bool

	// Drop delivers the files by synthesized drag-and-drop onto an element
	// instead of setting a file input's FileList — for the growing number of
	// apps whose only upload affordance is a drop zone with no <input
	// type=file> behind it. DropAt does the same at a viewport coordinate.
	//
	// The events are synthesized but the File objects are real: they come from
	// DOM.setFileInputFiles on a temporary hidden input, so a handler reading
	// dataTransfer.files gets genuine file data, not a stub.
	Drop   string
	DropAt *Point

	Query QueryOpts
}

UploadOpts controls the `upload` verb — attaching local files to a file input via DOM.setFileInputFiles.

There is no "click the input first" option and there never will be: clicking a file input opens the NATIVE OS file dialog, which lives outside the page, is invisible to CDP, blocks the browser's main thread, and — unlike a JavaScript dialog, which --on-dialog handles — has no CDP method that can dismiss it.

type WaitCond

type WaitCond struct {
	URL     string
	Visible string
	Gone    string
	Text    string // until the accessibility tree contains this text (e.g. a "Success" toast)
	Stable  bool   // until the accessibility tree stops changing (the page settled)
	Idle    bool   // until network activity settles (no in-flight requests for a window)
	Query   QueryOpts
}

WaitCond is a condition for the `wait` verb: settle until the target URL contains URL, or a selector becomes Visible / is Gone. Exactly one is set; Query carries the selector options for Visible/Gone. A fixed --for duration is handled in the CLI (no browser round-trip needed).

type WindowBounds added in v0.2.0

type WindowBounds struct {
	Left   int64  `json:"left"`
	Top    int64  `json:"top"`
	Width  int64  `json:"width"`
	Height int64  `json:"height"`
	State  string `json:"state"`
}

WindowBounds is a real Chrome window's position, size, and state — as read back AFTER any resize, because the OS may clamp a request to the screen.

type WindowOpts added in v0.2.0

type WindowOpts struct {
	Width  int64
	Height int64
}

WindowOpts controls the `window` verb. The zero value means "report the current bounds"; a non-zero Width/Height resizes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL