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

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):

  1. Emulation.setDeviceMetricsOverride (width, height, deviceScaleFactor, mobile).
  2. Emulation.setEmulatedMedia with prefers-color-scheme: light|dark.
  3. Page.addScriptToEvaluateOnNewDocument installing the stabilising CSS (animations/transitions off, caret hidden, hide selectors visibility:hidden) before the page's own scripts run; it is injected again after load as a safety net.
  4. Page.navigate, then poll document.readyState === 'complete' with Runtime.evaluate, then document.fonts.ready.
  5. Waits: selector polling, resource-timeline quiet period, fixed delay.
  6. Actions. Click and hover use Input.dispatchMouseEvent at the element centre after scrolling it into view, so :hover styles and real event handlers fire. Typing focuses the element and uses Input.insertText.
  7. Masks: for every element matching a mask selector an absolutely positioned magenta div is appended at the element's document coordinates.
  8. Page.captureScreenshot: - viewport: no clip; - full page: scroll to top, take cssContentSize from Page.getLayoutMetrics, captureBeyondViewport: true with a clip covering the whole document; - element: scroll into view, clip to getBoundingClientRect() plus the scroll offsets.
  9. 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