CDPEx.Protocol (CDPEx v0.1.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 websocket 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(), {: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.

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

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(arg)

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

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.