len(_cdp_domains), [d['domain'] for d in _cdp_domains[:5]](55, ['Accessibility', 'Animation', 'Audits', 'Autofill', 'BackgroundService'])
(55, ['Accessibility', 'Animation', 'Audits', 'Autofill', 'BackgroundService'])
Search CDP domains and commands by name or description
'Audits.checkFormsIssues: Runs the form issues check for the target page. Found issues are reported\nu'
Connection info from contents p, profile dir d, or the default Chrome profile
Chrome (146+) can expose CDP from your everyday browser: enable Allow remote debugging in chrome://inspect. Chrome then accepts WebSocket connections on the endpoint recorded in its profile’s DevToolsActivePort file – port on the first line, WebSocket path on the second – and asks you to approve each newly connecting client. Only that WebSocket endpoint is served (the /json/* HTTP interface belongs to the debug-instance mechanism described later). cdp_conninfo reads the file, and CDP.connect() uses it by default.
On MacOS, the connection info is stored in ~/Library/Application Support/Google/Chrome/DevToolsActivePort (or ~/Library/Application Support/Chromium/DevToolsActivePort for Chromium).
Chrome DevTools Protocol connection with event support
The transport is pluggable. _send (command frame in, reply frame out) and _dispatch (route an event frame to subscribed queues) are the only two methods that touch the websocket protocol flow, so an alternative transport subclasses CDP, overrides _send and the connection lifecycle, and feeds incoming events to _dispatch. Everything else (domain proxies, helpers, event buffers, Page) is inherited unchanged. solvecdp does exactly this, relaying frames through a solveit server to a Chrome extension.
Connect via Chrome remote debugging HTTP endpoint
remote targets the other mechanism: a separate “debug Chrome” started with a remote debugging port. fastcdp-setup creates a “CDP Chrome” launcher for one on remote’s default port 9223 (not 9222, which a main browser with built-in debugging enabled already holds). Since Chrome 136 the debugging switches are ignored for your everyday profile – a debug instance must point --user-data-dir at a non-standard directory, so that scripts can’t reach your real profile’s (differently-encrypted) cookies. To start one by hand on macOS:
That instance serves the classic HTTP metadata endpoints, and remote reads webSocketDebuggerUrl from /json/version to connect – no approval prompts, since the profile is disposable.
The third connection option: start the user’s installed Chrome ourselves, CDP-ready – no manual setup, no approval popups. launch runs it on a separate profile directory (Chrome requires a non-default one for debugging) with an ephemeral debug port, and quit shuts it down again. One launched instance per profile dir: a second launch on the same dir would just signal the running instance and exit.
We launch headless=True here so running this notebook’s tests doesn’t pop up a browser window; drop it when you want to watch.
Path of the installed Chrome/Chromium binary ($FASTCDP_CHROME overrides)
async def launch(
user_data_dir:str | pathlib.Path=None, # Profile dir; `~/.cache/fastcdp/profile` if None
headless:bool=False, # Run without a visible window?
debug:bool=None, # Print protocol events?
timeout:int=10, # Seconds to wait for the debug endpoint
reuse:bool=True, # Connect to an instance already running on this profile? (Else raise)
):Launch the installed Chrome CDP-ready on its own profile dir, and connect to it
Quit the browser, wait until it has released its debug port, and close the connection
A second launch on the same profile connects to the running instance instead of failing (reuse=False to make it an error). The reattached handle has no proc, since the process belongs to whoever launched it, and quit works regardless. This is the recovery path when a kernel restart orphans a launched browser.
A quit Chrome leaves DevToolsActivePort behind, so launch trusts the file only after checking its port with is_port_free: a free port proves the file stale, and launch removes it and starts fresh.
The browser’s open page targets
Page targets as attribute-access rows, one line per target
Initialize self. See help(type(self)) for accurate signature.
Initialize self. See help(type(self)) for accurate signature.
Evaluate expr in the page, raising on a JS exception
Attach to target tid
'Example Domain'
Call self as a function.
Call self as a function.
Wait for CSS selector to match an element
Wait for JS expression to be truthy, return its value
Call self as a function.
True
Initialize self. See help(type(self)) for accurate signature.
Initialize self. See help(type(self)) for accurate signature.
Connect via remote debugging and return a Page for the active tab
A Page driving the focused tab, or None when no tab has focus
remote_page is the quickest start against a debug Chrome: connect and drive whichever tab is focused.
Create a new tab, return Page
True
attach_page is new_page’s counterpart for a tab that already exists: the same Page proxy, bound to the target you name.
Attach to the existing tab tid
Navigate to url and wait for load+idle, raising on a navigation error
Context manager: subscribes before action, waits for load+idle after
Wait until network is idle for idle_ms
Chrome reports a refused navigation in the command result (errorText) rather than as a protocol error, so goto checks for it and raises at once — the alternative is waiting out a load event that can never come. One notable refusal: transports that attach via chrome.debugger (the extension path) get renderer-initiated semantics, where top-frame data: URLs are banned. For “this HTML, in this page” — test fixtures, generated reports — set_content writes the document directly, no navigation involved.
Replace the page’s document with html (via Page.setDocumentContent); no navigation happens
Screenshot of the viewport, or the whole scrollable page if full
''
{'nodeId': '2',
'ignored': False,
'role': {'type': 'internalRole', 'value': 'RootWebArea'},
'chromeRole': {'type': 'internalRole', 'value': 144},
'name': {'type': 'computedString',
'value': '',
'sources': [{'type': 'relatedElement', 'attribute': 'aria-labelledby'},
{'type': 'attribute', 'attribute': 'aria-label'},
{'type': 'attribute', 'attribute': 'aria-label', 'superseded': True},
{'type': 'relatedElement', 'nativeSource': 'title'}]},
'properties': [{'name': 'focusable',
'value': {'type': 'booleanOrUndefined', 'value': True}},
{'name': 'focused', 'value': {'type': 'booleanOrUndefined', 'value': True}},
{'name': 'url',
'value': {'type': 'string', 'value': 'https://httpbingo.org/forms/post'}}],
'childIds': ['18'],
'backendDOMNodeId': 2,
'frameId': 'B327EB775230754D1A31282EDBCC91BD'}
Chrome accessibility tree node with compact repr
Chrome accessibility tree node with compact repr
Build AXNode tree from flat CDP accessibility node list
Get accessibility tree for session
focusable=True focused=True url=https://httpbingo.org/forms/post [#2]
focusable=True editable=plaintext settable=True [#6]focusable=True editable=plaintext settable=True [#7]focusable=True editable=plaintext settable=True [#8]focusable=True [#10]focusable=True [#11]focusable=True [#12]focusable=True [#13]focusable=True [#14]focusable=True [#15]focusable=True [#16]focusable=True settable=True [#17]
focusable=True settable=True valuemin=1 valuemax=12 [#53]
focusable=True settable=True valuemax=59 [#55]
focusable=True settable=True valuemin=1 valuemax=2 [#57]
focusable=True hasPopup=menu [#3]focusable=True editable=plaintext settable=True multiline=True [#9]focusable=True [#62]
Find all descendants matching role and/or name substring
Find first descendant matching role and/or name substring
Find first descendant matching role and/or name substring
find/find_id target a control you already know is there. Orientation on an unfamiliar page is the inverse problem — where is the content, and what surrounds it? grep regex-searches every node name and returns one line per hit: the #id drops straight into click/fill_text, and the ancestor path says where on the page the hit sits (InlineTextBox layout artifacts are skipped, since each just duplicates its parent’s text). From a leaf hit, up climbs to the enclosing widget, and view renders a subtree to a bounded depth. So the reading workflow is: grep to locate, up/view to read just that neighborhood — never the whole tree.
Regex-search descendant names, for orientation: hits carry ids and ancestor paths
grep hits, one line per node: id, role, name, ancestor path
Markdown subtree rooted here, to depth levels (None = unbounded)
A rendered subtree, displayed as markdown
Ancestor chain as a ’ > ’ joined summary, root first
The nth ancestor (None past the root)
role= narrows a text match that lands on several node kinds, and view with a depth reads a hit’s neighborhood without dumping its whole subtree — elided levels end in …:
A hit is often a leaf inside the widget that matters: up climbs to it, and path names where in the page a node sits.
The ax tree and the DOM speak different id spaces: ax nodes carry backend ids, which are what click and fill_text take, while the DOM/CSS domains want the front-end nodeId. sel_node resolves a CSS selector to a nodeId. matched_styles answers the design question computed styles can’t – why an element looks the way it does – as every matching rule in cascade order (winners last) with its origin. It takes a selector or an ax backend id, so a grep or find_id hit can be interrogated directly.
Matching CSS rules for a selector or an ax backend node id, with each rule’s origin
Matched rules in cascade order (winners last), one line per rule
DOM nodeId of the first element matching CSS selector sel
Click a DOM node
Run code with a DOM node as this
Call function fn on a DOM node, returning the protocol result
{}
Call self as a function.
{'type': 'undefined'}
Click element and wait for load+idle
click_and_wait covers clicks that navigate. When a click swaps content in place (tab panels, htmx, SPAs), there is no navigation to wait for, and the tree in hand goes stale. wait_for_ax polls until a node matching role/name (as in find) exists and returns the fresh tree — the wait and the re-read are one call.
Poll ax_tree until a node matches role/name (as in find); returns the fresh tree
Helpers for debugging apps: buffered console and network history, auto-handled dialogs, and a few conveniences. CDP only delivers events once the relevant domain is enabled, so each start_* helper enables it and buffers from that moment on.
Console/exception messages buffered since start_console, filtered by regex pattern
Enable and start buffering console messages and uncaught exceptions
start_console begins capture, and console returns everything seen so far; error: entries are uncaught exceptions, with their stack. Messages logged before start_console was called are never seen, so call it right after creating a page.
['log: hello 42',
'warning: watch out',
'error: Error: boom\n at <anonymous>:1:26']
The pattern regex filters entries:
Body of a response seen by start_network, decoded if base64
(status,url,requestId) of responses buffered since start_network, url filtered by regex pattern
Enable and start buffering network responses
requests answers “what did the page load, and with what status?”, and response_body fetches a body by the returned request id (Chrome only keeps bodies while the page is alive).
[(200, 'https://httpbingo.org/forms/post', 'A0012C0BE59AE318F96FC9B3C00B7D1A')]
Auto-respond to JS dialogs from now on, recording (type,message) in dialogs
Without a handler, an unexpected alert or confirm blocks the page – and any eval that triggered it – forever. After handle_dialogs, dialogs are answered as they open: accept=False dismisses them, and text fills prompts.
(True, [('confirm', 'Proceed?')])
Wait for text to appear in (with present=False, disappear from) the page body
Set a <select> element’s value and fire its change event
click on a <select> doesn’t open native dropdowns under CDP, so select_option sets the value directly (firing change so the app reacts). wait_for_text complements wait_for_selector when the interesting change is text, e.g. htmx swaps. And screenshot above takes full=True for the whole scrollable page.
[884, 6070]
True
To finish, exercise the browser we launched at the start end to end, then quit it:
Allow all CDP classes in safepyrun