Hex.pm Docs CI License

OTP-native Chrome DevTools Protocol browser automation for Elixir. Launch headless Chrome and drive it directly over a Mint.WebSocket connection — no ChromeDriver, no Node.js.

CDPEx.with_page([], fn page ->
  {:ok, _} = CDPEx.Page.navigate(page, "https://example.com")
  CDPEx.Page.html(page)
end)
#=> {:ok, "<html>…</html>"}

Why CDPEx?

It drives Chrome over CDP the way Puppeteer and Playwright do — but it's pure Elixir: the browser and each page's CDP connection are supervised OTP processes (a page is a lightweight handle over its connection). A Chrome crash or a dropped socket surfaces to the caller as {:error, reason} instead of a hung session, and terminate/2 guarantees the OS process is reaped (no zombie Chromes).

CDPExchrome_remote_interfaceChromicPDFWallaby
TransportCDP (WebSocket)CDP (WebSocket)CDP (WebSocket)WebDriver / ChromeDriver
Runtime depsmint_web_socket, jasonhackney + othersa fewChromeDriver process
Supervised lifecycle✅ (PDF pool)partial
Scopegeneral automationlow-level clientPDF / screenshotstesting
Node.js requirednononono

If you want a small, dependency-light CDP client with proper OTP supervision — and you don't want a ChromeDriver process or a Node sidecar — that's the gap CDPEx fills.

Status

v0.1 is single-browser, one-WebSocket-per-page, headless Chrome only. Connection pooling, sessionId multiplexing, network interception, and stealth are intentionally out of scope for this release.

Installation

Add cdp_ex to your deps in mix.exs:

def deps do
  [
    {:cdp_ex, "~> 0.1"}
  ]
end

You also need Chrome or Chromium installed. CDPEx finds it via, in order: the :chrome_binary option, CDP_EX_CHROME_BINARY, CHROME_BINARY, then an OS default. For reproducible setups, point it at a Chrome for Testing binary.

Usage

with_page/3 opens a page, runs your function, and always tears everything down — even if the function raises:

# Throwaway browser + page for one job:
{:ok, title} =
  CDPEx.with_page([], fn page ->
    {:ok, _} = CDPEx.Page.navigate(page, "https://example.com")
    CDPEx.Page.evaluate(page, "document.title")
  end)

Explicit lifecycle

{:ok, browser} = CDPEx.launch(headless: true)
{:ok, page}    = CDPEx.new_page(browser)

{:ok, _page} = CDPEx.Page.navigate(page, "https://example.com")
:ok          = CDPEx.Page.wait_for_selector(page, "h1")
{:ok, html}  = CDPEx.Page.html(page)
{:ok, "Example Domain"} = CDPEx.Page.evaluate(page, "document.querySelector('h1').textContent")
{:ok, _png}  = CDPEx.Page.screenshot(page, path: "example.png")

:ok = CDPEx.close_page(browser, page)
:ok = CDPEx.stop(browser)

Under your supervision tree

Because terminate/2 reaps Chrome, supervise the browser with a :shutdown timeout (not :brutal_kill):

children = [
  {CDPEx.Browser, name: MyBrowser, headless: true}
]
Supervisor.start_link(children, strategy: :one_for_one)

Page operations

FunctionDescription
navigate/3Go to a URL, waiting for networkAlmostIdle (configurable)
wait_for_selector/3Poll until a CSS selector matches
evaluate/3Run JS and return the value (returnByValue)
click/3Synthetic .click() on the first match
html/2Full serialized DOM (document.documentElement.outerHTML)
screenshot/2PNG bytes, or write to :path

Full API: hexdocs.pm/cdp_ex.

Development

mix deps.get
mix test                         # unit tests (no Chrome needed)
mix test --include integration   # real-Chrome tests (set CDP_EX_CHROME_BINARY)
mix ci                           # format, credo, dialyzer, unit tests

Integration tests are tagged :integration and excluded by default; they launch a real Chrome and drive it against a local fixture HTTP server.

Acknowledgements

Built on mint_web_socket. Inspired by the production CDP work in ChromicPDF and by Puppeteer's protocol layer.

License

MIT — see LICENSE.