Skip to main content
Info

In short: the developer tools (DevTools) are a set of panels built into Chrome, Edge, Firefox and Safari that show what a page is made of and what it is doing. In Chrome they are a client of the Chrome DevTools Protocol (CDP): JSON messages over a WebSocket, with no driver in between. Any program can speak the same protocol. The pure CDP engine (seleniumbase_rs::sb_cdp) does, and CDP mode adds it to a WebDriver session. This page has both halves: the panels you open by hand when a test misbehaves, and the protocol behind every one of them.

Opening the tools#

Press F12, or Ctrl+Shift+I (Windows and Linux) or Cmd+Option+I (macOS), or right-click an element and choose Inspect. A panel opens beside or below the page. In Chrome, the tools are themselves a web page that talks to the page you inspect over the Chrome DevTools Protocol, so anything the panels can do, a program can do too.

The panels#

PanelWhat it showsA use in testing
ElementsThe live DOM and the CSS rules that apply to each nodeFind a stable selector, see why an element is hidden
ConsoleMessages from the page, and a prompt that runs JavaScriptTry a selector, read errors, run a one-line check
SourcesThe page’s scripts, with breakpoints and steppingFind out which script changes a value
NetworkEvery request and response, with timing, headers and bodiesFind the API call behind a button, see a failure
PerformanceA recording of what the browser did, frame by frameExplain a page that is slow to become ready
MemoryHeap snapshotsHunt a leak in a long-running page
ApplicationCookies, local storage, IndexedDB, service workersCheck login state, see what a session keeps
SecurityCertificate and mixed-content informationUnderstand a certificate error
LighthouseAn audit for speed, accessibility and best practiceA quick quality check
RecorderRecords clicks and typing as a replayable flowA first draft of a test

Most of this page is about two of them, Console and Network, because they are the two you will open most often when a test misbehaves.

The Console in depth#

The Console has two jobs: it shows what the page logs, and it runs what you type, in the page’s own context, with the page’s own variables.

Levels#

Every message has a level. The filter menu at the top of the panel shows or hides each level.

JavaScript callLevel in the filterLooks like
console.debug(...)VerboseGrey text, hidden by default
console.log(...) and console.info(...)InfoPlain text
console.warn(...)WarningsYellow, with a warning icon
console.error(...)ErrorsRed, with a stack trace

The browser also writes to the Console by itself, always at the Error or Warning level: an uncaught exception in a script, a promise rejected with nothing to handle it, a failed request (Failed to load resource), a mixed-content or CORS block, and a deprecation notice. For a test these are valuable: a page that renders fine but logs a red error is often the first sign of a bug.

Helpers that only exist in the Console#

The Console has a few shortcuts that are not part of the page. A script on the page cannot use them, only the prompt can.

HelperWhat it does
$0The element currently selected in the Elements panel. $1 to $4 are the ones selected before
$_The value of the last expression you ran
$('css')Shortcut for document.querySelector('css'): the first match
$$('css')Shortcut for document.querySelectorAll('css'), but returns a real array, so map and filter work
$x('//xpath')Evaluates an XPath expression and returns an array of nodes
copy(value)Puts a value on the clipboard, handy for a large JSON
inspect(node)Selects a node in the Elements panel
getEventListeners(node)Lists the event listeners attached to a node
monitorEvents(node, 'click')Logs every event of that type on the node

A typical selector check takes ten seconds. Select the button in the Elements panel, then type:

If the length is 0 or 2, the selector is wrong for a test whatever it looks like on screen. Remember that the Console runs in the top frame by default; a selector for something inside an iframe needs the context menu at the top of the panel switched to that frame, just as automation must switch to the frame first.

Other useful calls#

console.table(array) prints an array of objects as a table, console.group and console.groupEnd indent related lines, console.time('x') with console.timeEnd('x') measures a duration, console.assert(condition, 'message') logs only when the condition is false, and console.trace() prints the call stack at that point. Tick Preserve log to keep messages across a page navigation, which is what you want while debugging a login flow.

Network and Copy as cURL#

The Network panel lists every request the page makes. Click one to see its request headers, response headers, payload and response body. The filter row narrows the list to Fetch/XHR (the API calls of a modern app, see frontends, backends and APIs), documents, scripts or images.

Right-click a request and Copy offers several forms. Copy as cURL produces a command you can paste into a terminal and run, with the method, URL, headers, cookies and body of that exact request:

Warning

A copied request includes your cookies and authorization headers. They are real credentials for your account. Do not paste them into a ticket, a chat or a repository. Replace them with a placeholder, or log in with a throwaway test account when you build a request you plan to share.

Reproducing a request with curl answers a useful question: does the call work without the browser? If it does, a test may not need a browser for that step at all; if it fails, the difference between the browser’s request and yours (a header, a cookie, a token) is the thing to find. Copy as fetch gives the same request as JavaScript, and Save all as HAR writes the whole list to a file that other tools can read, as described in Network.

Where CDP comes from#

The developer tools above are a web page that is not built into the browser’s engine: they talk to the page you are inspecting over a protocol, the Chrome DevTools Protocol. They read the DOM, list network requests, set breakpoints and take screenshots by sending that protocol’s messages.

Because the protocol is just messages over a network connection, any program can be the “developer tools”. Puppeteer, Playwright and the pure CDP engine of seleniumbase-rs are such programs. The protocol belongs to Chromium (Chrome, Edge, Brave, Opera), is versioned with the browser and is not a web standard. Its definition is public (the “protocol viewer” on the Chrome DevTools site), and some of its parts are marked experimental or deprecated and do change.

Connecting#

Chrome opens a debugging port

Started with --remote-debugging-port, Chrome listens for connections on that port, on the local machine only by default. With the port set to 0, Chrome picks a free one and writes it to a file in its profile folder (DevToolsActivePort). No other program is needed.

Ask where to connect

A plain HTTP request to the port returns what the client needs: GET /json/version gives the browser’s version and its webSocketDebuggerUrl; GET /json/list lists the open tabs, each with its own WebSocket address.

Open a WebSocket

The client connects to that ws:// address. From now on everything, in both directions, is JSON text frames on that one connection.

The remote debugging port#

The tools above are a client. What they connect to is a debugging port that the browser itself opens. When the tools are built into the same browser you barely notice it, but you can start Chrome with a port and connect to it from another program, or from the DevTools of another Chrome:

Then open http://127.0.0.1:9222/json/version in any browser to see the version and the WebSocket address, or open chrome://inspect in another Chrome to attach to the tabs. The messages on that connection are the ones described in The messages below.

Several clients can attach to the same tab at once, so a person can watch the panels while a program drives the page.

Warning

Whoever can reach the debugging port controls the whole browser: its pages, its cookies and its logins. Chrome listens on the local machine only by default, and recent versions refuse the port for the browser’s default profile, which is why the command above names a separate --user-data-dir. Use a throwaway profile and never expose the port to a network.

The messages#

There are exactly three kinds of message.

A command is a method named Domain.command plus its params. The id lets a client keep many commands in flight at once and match each reply to its caller. Events are how the browser tells the client something happened. Most events arrive only after the client turns the domain on with an enable command, such as Network.enable, so a busy page does not flood a client that did not ask.

Domains#

The protocol is split into domains, one for each part of the browser. These are the ones that matter for automation:

DomainWhat it controlsExamples
BrowserThe browser process: version, windows, permissionsBrowser.getVersion, Browser.grantPermissions
TargetTabs, iframes and workers, and attaching to themTarget.createTarget, Target.attachToTarget, Target.createBrowserContext
PageNavigation, the page lifecycle, screenshots, PDFPage.navigate, Page.captureScreenshot, Page.addScriptToEvaluateOnNewDocument; events Page.frameNavigated, Page.loadEventFired
RuntimeRunning JavaScript, and handles to objects in the pageRuntime.evaluate, Runtime.callFunctionOn; event Runtime.consoleAPICalled
DOMThe document treeDOM.getDocument, DOM.querySelector, DOM.getBoxModel
InputTrusted mouse, keyboard and touch eventsInput.dispatchMouseEvent, Input.dispatchKeyEvent
NetworkRequests, responses, cookies, throttlingNetwork.enable, Network.emulateNetworkConditions; events Network.requestWillBeSent, Network.responseReceived
FetchPausing and rewriting requestsFetch.enable, Fetch.fulfillRequest; events Fetch.requestPaused, Fetch.authRequired
EmulationPretending to be another device or placeEmulation.setDeviceMetricsOverride, Emulation.setTimezoneOverride, Emulation.setUserAgentOverride
StorageCookies and storage of an originStorage.getCookies, Storage.clearDataForOrigin

Targets and sessions#

A running Chrome is not one thing to talk to but many. Each tab, each cross-site iframe, each service worker and shared worker, and the browser itself is a target. To command one, the client attaches to it:

With flatten: true, the sessions of all tabs share the one connection, and the sessionId field says whom a message is for or from. A session is a private conversation with one target: enabling Network in one tab does not start network events in another.

The browser also has browser contexts, the protocol’s name for what Chrome shows as an incognito window. Target.createBrowserContext makes one; targets created inside it share cookies and storage with each other and with nothing else. See Sessions, cookies and storage.

What a page load looks like#

Navigation in CDP: the reply to Page.navigate only says the navigation started. The page is loaded when the events arrive.

That is why a good client subscribes before it navigates: the load event can arrive before the reply to the command does. If a navigation fails (no network, a bad certificate), the reply carries an errorText such as net::ERR_NAME_NOT_RESOLVED. See how a page loads for what those events mean.

What a click looks like#

CDP has no “click this element” command. A click is built from smaller pieces:

A typical click. Many clients also scroll the element into view first.

Input.dispatchMouseEvent goes through the browser’s real input path, so the page sees ordinary events whose isTrusted property is true. A script that only calls element.click() produces an untrusted event, which some sites refuse.

JavaScript results come back as remote objects: Runtime.evaluate returns a description (type and value), or, for an object that cannot be copied, an objectId the client can pass to later commands such as Runtime.callFunctionOn. With returnByValue the browser copies the value into the reply.

CDP compared with WebDriver#

WebDriverCDP
Between test and browserA driver process (chromedriver)Nothing
TransportOne HTTP request per commandOne WebSocket for everything
Who starts the browserThe driverYou (or your library) start Chrome with a debugging port
Events from the pageNone (classic); some with BiDiNetwork, console, page loads, targets
LevelHigh: “click this element”, “get the title”Low: mouse events, DOM nodes, raw requests
StandardW3C, the same for every browserChromium only, versioned with the browser
BrowsersEvery major browserChromium-based browsers
In seleniumbase-rsBaseCasesb_cdp::Browser, Page, Locator

CDP can do things the classic WebDriver protocol cannot: pause and rewrite requests, set the time zone, emulate a phone, print to PDF, record a HAR. The price is that every higher-level behaviour (waiting for an element, scrolling before a click) is the client’s job. WebDriver BiDi is the standards-based effort to bring the event-style features to every browser.

How seleniumbase-rs uses it#

The pure CDP engine#

The pure CDP engine lives in seleniumbase_rs::sb_cdp.

The pure CDP engine and Chrome

  • One connection. The client in src/sb_cdp/client.rs keeps one WebSocket for every tab. A background task reads it, completes the pending request that matches each reply id, and broadcasts everything else as an event, so commands from different tabs never block one another. A command that gets no reply is abandoned after 30 seconds.
  • Launching. Browser::launch(LaunchOptions) starts Chrome with a debugging port and waits for it to publish its endpoint (20 seconds by default; startup_timeout changes that). LaunchOptionsBuilder::debugging_port fixes the port instead of letting Chrome choose.
  • Tabs and contexts. Browser::default_page() returns the first tab as a Page; Browser::new_context() creates an isolated BrowserContext.
  • Raw commands. Page::execute(method, params) sends any command to one tab, Browser::execute sends one to the browser itself, and Page::events() gives an Events handle for the tab’s protocol events.
  • Navigation. Page::goto follows the order described above: it listens first, sends Page.navigate, reads errorText and loaderId from the reply, and waits for the load event. A change of only the #fragment has no loaderId and fires no load event, so it returns at once.
  • Input. Page::mouse() and Page::keyboard() send Input.dispatchMouseEvent and Input.dispatchKeyEvent, so clicks and keystrokes are trusted events. A Locator uses them for you.
  • Other domains. Interception uses Fetch (Network), Page::apply_fingerprint uses Page.addScriptToEvaluateOnNewDocument and Emulation (Detection and spoofing), and Page::emulation() exposes time zone, locale, geolocation, user agent and offline mode.

A Locator is lazy: it finds the element again each time it acts, and waits for it. That removes the stale-reference problem of WebDriver.

src/main.rs
Note

The pure CDP engine also has a mock transport (Browser::new_mocked, which returns the browser and a MockCtrl; it needs the test-util feature), so code that drives pages can be tested without starting Chrome.

DevTools from the command line, Rust and MCP#

The project does not draw its own tools. It starts a browser the real DevTools can reach and gives programs and AI assistants the same information.

sbrs devtools launches Chrome with the pure CDP engine, listening on 127.0.0.1 port 9222 (--port 0 lets Chrome pick a free one). It prints the HTTP endpoint, the browser WebSocket address and ready-to-paste configurations for Google’s chrome-devtools-mcp and Microsoft’s Playwright MCP, then keeps the browser open until you press Ctrl+C. --json prints the same information as one line of JSON.

While it runs, open chrome://inspect in another Chrome, or point an MCP client at the endpoint, and look at the very same tab.

Debugging tools for MCP clients#

The MCP servers of sbrs-mcp (see MCP) offer the pieces of the panels as tools, so an assistant can look at a page the way you would:

ToolPanel it stands in for
get_console_messagesConsole. start begins recording, level filters (for example error), limit and clear bound and reset the list
get_failed_requestsNetwork. Lists requests that failed or returned 400 and up; watch records every failure on the pure CDP engine
inspect_pageElements. An outline of the page
diagnose_selectorConsole $$. Says why a selector matches nothing or too much
capture_debug_bundleSeveral panels at once, saved for a bug report
get_devtools_endpointThe debugging port. Returns the same addresses and configs as sbrs devtools
Tip

When a test fails and you do not know why, work from cheap to costly. First read the console errors and the failed requests, because they often name the cause. Then check the selector with $$ in a headed browser. Only then reach for sources, breakpoints and the performance recording.

Where to go next#