CDP notes
snapdiff talks to Chrome through the DevTools Protocol directly (snapdiff/cdp.py), with only websockets as a dependency.
Launching
google-chrome --headless=new --remote-debugging-port=0 --user-data-dir=<fresh temp dir> \
--hide-scrollbars --force-color-profile=srgb --font-render-hinting=none --disable-lcd-text ... about:blank
--remote-debugging-port=0lets Chrome pick a free port; it printsDevTools listening on ws://127.0.0.1:PORT/devtools/browser/IDon stderr, which snapdiff parses. stderr is drained afterwards so Chrome never blocks on a full pipe.- Every run uses its own throw-away
--user-data-dir, deleted on exit, so runs do not share cookies, caches or first-run state, and parallel runs never fight over a profile. - The rendering flags remove common sources of cross-run noise: no scrollbars, a fixed colour profile, no font hinting or LCD sub-pixel text.
--no-sandboxis added only when running as root (typical in containers).
Protocol shape
One WebSocket to the browser endpoint. Tabs are created with Target.createTarget and attached with Target.attachToTarget {flatten: true}; commands for a tab carry its sessionId. This gives N independent tabs over a single connection, which is what capture --concurrency uses. Each message has an id; a reader task resolves the matching future, turns error replies into CDPError, and fails pending calls if the socket closes.
Capture sequence
Per screenshot (capture.capture_one):
Emulation.setDeviceMetricsOverride(width, height,deviceScaleFactor,mobile).Emulation.setEmulatedMediawithprefers-color-scheme: light|dark.Page.addScriptToEvaluateOnNewDocumentinstalling the stabilising CSS (animations/transitions off, caret hidden,hideselectorsvisibility:hidden) before the page's own scripts run; it is injected again after load as a safety net.Page.navigate, then polldocument.readyState === 'complete'withRuntime.evaluate, thendocument.fonts.ready.- Waits: selector polling, resource-timeline quiet period, fixed delay.
- Actions. Click and hover use
Input.dispatchMouseEventat the element centre after scrolling it into view, so:hoverstyles and real event handlers fire. Typing focuses the element and usesInput.insertText. - Masks: for every element matching a
maskselector an absolutely positioned magentadivis appended at the element's document coordinates. Page.captureScreenshot: - viewport: no clip; - full page: scroll to top, takecssContentSizefromPage.getLayoutMetrics,captureBeyondViewport: truewith aclipcovering the whole document; - element: scroll into view, clip togetBoundingClientRect()plus the scroll offsets.Target.closeTarget.
Failures retry up to retries times with a fresh tab each time.
Fallback
If Chrome cannot be driven (no DevTools URL, connection failure), or a capture keeps failing, snapdiff tries google-chrome --headless=new --screenshot=FILE --window-size=WxH --force-device-scale-factor=N --virtual-time-budget=5000 URL. It is only used for pages with no actions, masks, hidden elements, selector waits, element mode, or dark scheme, because the CLI cannot express those, and it captures the viewport only.
Gotchas
- The reader task must treat
ConnectionClosedas normal:Browser.closemakes Chrome drop the socket. - Static server threads must not block shutdown: Chrome keeps idle connections open, so the server uses daemon threads.
- Chrome's
clipuses CSS pixels; withdeviceScaleFactor: 2the PNG is twice the clip size. - Screenshots of pages taller than a few tens of thousands of pixels fail in Chrome, hence the 16000 px cap.