Pure CDP wire helpers: JSON-RPC encoding, frame decoding, and message classification. No process state, no I/O — every function here is referentially transparent and unit-testable without a running Chrome.
Module note: this module aliases
Mint.WebSocketasWebSocket.
The Chrome DevTools Protocol is JSON-RPC 2.0-ish over a WebSocket:
- a command is
%{"id" => n, "method" => "Domain.method", "params" => %{}} - a reply echoes the
"id"and carries either"result"or"error" - an event has a
"method"and"params"but no"id"
CDPEx.Connection owns the socket and drives these helpers.
Summary
Functions
Classifies one decoded frame into a CDP-level action.
Decodes Mint.WebSocket stream responses for ref into a flat list of frames.
Encodes a CDP command to JSON iodata.
Unwraps a Runtime.evaluate result.
Splits a Chrome DevTools websocket URL into {host, port, path}.
JavaScript that neutralises alert/confirm/prompt so modal dialogs can't
block automation. Injected via Page.addScriptToEvaluateOnNewDocument.
Types
@type classification() :: {:reply, id :: pos_integer(), {:ok, map()} | {:error, map()}} | {:event, method :: String.t(), params :: map()} | {:ping, binary()} | {:close, code :: integer() | nil, reason :: binary()} | :ignore
The result of classifying a single decoded frame.
@type frame() :: Mint.WebSocket.frame() | {:close, integer() | nil, binary()}
A decoded Mint.WebSocket frame.
Functions
@spec classify(frame()) :: classification()
Classifies one decoded frame into a CDP-level action.
Text frames are parsed as JSON and split into replies (by "id") and events
(by "method"). Ping frames surface so the connection can pong, and a peer
close frame surfaces so the connection can shut down and fail pending
callers. Everything else (pong, unrecognised JSON) is :ignore.
For an error reply, the raw CDP error object is returned; the connection wraps
it with the originating method as {:cdp_error, method, error}.
Examples
iex> CDPEx.Protocol.classify({:text, ~s({"id":1,"result":{"frameId":"A"}})})
{:reply, 1, {:ok, %{"frameId" => "A"}}}
iex> CDPEx.Protocol.classify({:text, ~s({"id":2,"error":{"code":-32000,"message":"boom"}})})
{:reply, 2, {:error, %{"code" => -32000, "message" => "boom"}}}
iex> CDPEx.Protocol.classify({:text, ~s({"method":"Page.loadEventFired","params":{"t":1}})})
{:event, "Page.loadEventFired", %{"t" => 1}}
iex> CDPEx.Protocol.classify({:text, ~s({"method":"Inspector.detached"})})
{:event, "Inspector.detached", %{}}
iex> CDPEx.Protocol.classify({:ping, "hi"})
{:ping, "hi"}
iex> CDPEx.Protocol.classify({:close, 1000, "bye"})
{:close, 1000, "bye"}
iex> CDPEx.Protocol.classify({:pong, "hi"})
:ignore
@spec decode_frames(Mint.WebSocket.t(), [term()], reference()) :: {:ok, Mint.WebSocket.t(), [frame()]} | {:error, term()}
Decodes Mint.WebSocket stream responses for ref into a flat list of frames.
Non-matching responses (other refs, status/headers) are ignored. Returns the advanced websocket along with the frames so the caller can thread state.
@spec encode(String.t(), map(), pos_integer(), String.t() | nil) :: iodata()
Encodes a CDP command to JSON iodata.
Pass a session_id to target a flattened session; omit it (the default) for
commands sent on a page or browser socket directly.
Examples
iex> CDPEx.Protocol.encode("Page.navigate", %{"url" => "https://x.test"}, 1)
...> |> IO.iodata_to_binary()
...> |> Jason.decode!()
%{"id" => 1, "method" => "Page.navigate", "params" => %{"url" => "https://x.test"}}
iex> CDPEx.Protocol.encode("Page.enable", %{}, 7, "SID")
...> |> IO.iodata_to_binary()
...> |> Jason.decode!()
%{"id" => 7, "method" => "Page.enable", "params" => %{}, "sessionId" => "SID"}
Unwraps a Runtime.evaluate result.
A thrown JS exception becomes {:error, {:evaluate_exception, details}}; a
returned value (with returnByValue: true) becomes {:ok, value}; undefined
becomes {:ok, nil}.
Examples
iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "string", "value" => "<html>"}})
{:ok, "<html>"}
iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "number", "value" => 42}})
{:ok, 42}
iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "undefined"}})
{:ok, nil}
iex> {:error, {:evaluate_exception, _}} =
...> CDPEx.Protocol.evaluate_result(%{"exceptionDetails" => %{"text" => "Uncaught"}})
@spec parse_ws_url(String.t()) :: {String.t(), pos_integer(), String.t()}
Splits a Chrome DevTools websocket URL into {host, port, path}.
Examples
iex> CDPEx.Protocol.parse_ws_url("ws://127.0.0.1:9222/devtools/browser/abc-123")
{"127.0.0.1", 9222, "/devtools/browser/abc-123"}
@spec prevent_alerts_js() :: String.t()
JavaScript that neutralises alert/confirm/prompt so modal dialogs can't
block automation. Injected via Page.addScriptToEvaluateOnNewDocument.