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 ws:// 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(), session_id :: String.t() | nil, {:ok, map()} | {:error, map()}} | {:event, method :: String.t(), session_id :: String.t() | nil, 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"), each carrying any "sessionId" (from flattened CDP sessions)
so the connection can route per session. 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, nil, {:ok, %{"frameId" => "A"}}}
iex> CDPEx.Protocol.classify({:text, ~s({"id":2,"error":{"code":-32000,"message":"boom"}})})
{:reply, 2, nil, {:error, %{"code" => -32000, "message" => "boom"}}}
iex> CDPEx.Protocol.classify({:text, ~s({"method":"Page.loadEventFired","params":{"t":1}})})
{:event, "Page.loadEventFired", nil, %{"t" => 1}}
iex> CDPEx.Protocol.classify({:text, ~s({"method":"Inspector.detached"})})
{:event, "Inspector.detached", nil, %{}}
iex> CDPEx.Protocol.classify({:text, ~s({"method":"Page.lifecycleEvent","sessionId":"S","params":{"x":1}})})
{:event, "Page.lifecycleEvent", "S", %{"x" => 1}}
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}. A result Chrome can only express as an unserializableValue
— NaN, Infinity, -0, or a BigInt — has no by-value value and becomes
{:error, {:unserializable_value, uv}}, carrying the raw unserializableValue
string. Anything else (an unrecognized result envelope) falls through to
{:error, {:unexpected_evaluate, _}}. (Inputs Chrome can't serialize at all,
like window or a circular object, never reach here — the Runtime.evaluate
call fails first; see CDPEx.Page.evaluate/3.)
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"}})
iex> CDPEx.Protocol.evaluate_result(%{"result" => %{"type" => "bigint", "unserializableValue" => "10n"}})
{:error, {:unserializable_value, "10n"}}
@spec parse_ws_url(String.t()) :: {String.t(), pos_integer(), String.t()}
Splits a Chrome DevTools ws:// URL into {host, port, path}.
Uses URI.parse/1, so IPv6 hosts, explicit ports, and paths are handled
correctly; a non-ws:// URL or one missing a host/port raises ArgumentError.
Only ws:// is accepted: CDPEx launches a local Chrome and talks plaintext to
its DevToolsActivePort (no TLS). wss:// — a remote/TLS DevTools endpoint —
has no entry point yet (tracked in
#73).
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"}
iex> CDPEx.Protocol.parse_ws_url("ws://[::1]:9222/devtools/browser/abc")
{"::1", 9222, "/devtools/browser/abc"}
@spec prevent_alerts_js() :: String.t()
JavaScript that neutralises alert/confirm/prompt so modal dialogs can't
block automation. Injected via Page.addScriptToEvaluateOnNewDocument.