CDPEx.Protocol (CDPEx v0.3.0)

Copy Markdown View Source

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.WebSocket as WebSocket.

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

Types

The result of classifying a single decoded frame.

A decoded Mint.WebSocket frame.

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:// (or wss://) URL into {host, port, path}.

JavaScript that neutralises alert/confirm/prompt so modal dialogs can't block automation. Injected via Page.addScriptToEvaluateOnNewDocument.

Types

classification()

@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.

frame()

@type frame() :: Mint.WebSocket.frame() | {:close, integer() | nil, binary()}

A decoded Mint.WebSocket frame.

Functions

classify(arg1)

@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

decode_frames(websocket, responses, ref)

@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.

encode(method, params, id, session_id \\ nil)

@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"}

evaluate_result(other)

@spec evaluate_result(map()) :: {:ok, term()} | {:error, term()}

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"}})

parse_ws_url(ws_url)

@spec parse_ws_url(String.t()) :: {String.t(), pos_integer(), String.t()}

Splits a Chrome DevTools ws:// (or wss://) URL into {host, port, path}.

Uses URI.parse/1, so IPv6 hosts, explicit ports, and paths are handled correctly; a non-ws(s):// URL or one missing a host/port raises ArgumentError.

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"}

prevent_alerts_js()

@spec prevent_alerts_js() :: String.t()

JavaScript that neutralises alert/confirm/prompt so modal dialogs can't block automation. Injected via Page.addScriptToEvaluateOnNewDocument.