Current section

Files

Jump to
phoenix_test_playwright lib phoenix_test playwright connection.ex
Raw

lib/phoenix_test/playwright/connection.ex

defmodule PhoenixTest.Playwright.Connection do
@moduledoc """
Stateful, `GenServer` based connection to a Playwright node.js server.
The connection is established via `Playwright.Port`.
You won't usually have to use this module directly.
`PhoenixTest.Case` uses this under the hood.
"""
use GenServer
alias PhoenixTest.Playwright.Port, as: PlaywrightPort
require Logger
@default_timeout_ms 2000
@playwright_timeout_grace_period_ms 500
defstruct [
:port,
status: :pending,
awaiting_started: [],
initializers: %{},
guid_ancestors: %{},
guid_subscribers: %{},
guid_received: %{},
posts_in_flight: %{}
]
@name __MODULE__
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: @name, timeout: timeout())
end
@doc """
Lazy launch. Only start the playwright server if actually needed by a test.
"""
def ensure_started(opts \\ []) do
case Process.whereis(@name) do
nil -> start_link(opts)
pid -> {:ok, pid}
end
GenServer.call(@name, :awaiting_started)
end
@doc """
Launch a browser and return its `guid`.
"""
def launch_browser(type, opts) do
types = initializer("Playwright")
type_id = Map.fetch!(types, type).guid
resp = post(guid: type_id, method: "launch", params: Map.new(opts))
resp.result.browser.guid
end
@doc """
Subscribe to messages for a guid and its descendants.
"""
def subscribe(pid \\ self(), guid) do
GenServer.cast(@name, {:subscribe, {pid, guid}})
end
@doc """
Post a message and await the response.
We wait for an additional grace period after the timeout that we pass to playwright.
"""
def post(msg) do
default = %{params: %{}, metadata: %{}}
msg = msg |> Enum.into(default) |> update_in(~w(params timeout)a, &(&1 || timeout()))
timeout = msg.params.timeout
timeout_with_grace_period = timeout + @playwright_timeout_grace_period_ms
GenServer.call(@name, {:post, msg}, timeout_with_grace_period)
end
@doc """
Get all past received messages for a playwright `guid` (e.g. a `Frame`).
The internal map used to track these messages is never cleaned, it will keep on growing.
Since we're dealing with (short-lived) tests, that should be fine.
"""
def received(guid) do
GenServer.call(@name, {:received, guid})
end
@doc """
Get the initializer data for a channel.
"""
def initializer(guid) do
GenServer.call(@name, {:initializer, guid})
end
@impl GenServer
def init(config) do
port = PlaywrightPort.open(config)
msg = %{guid: "", params: %{sdkLanguage: "javascript"}, method: "initialize", metadata: %{}}
PlaywrightPort.post(port, msg)
{:ok, %__MODULE__{port: port}}
end
@impl GenServer
def handle_cast({:subscribe, {recipient, guid}}, state) do
subscribers = Map.update(state.guid_subscribers, guid, [recipient], &[recipient | &1])
{:noreply, %{state | guid_subscribers: subscribers}}
end
@impl GenServer
def handle_call({:post, msg}, from, state) do
msg_id = fn -> System.unique_integer([:positive, :monotonic]) end
msg = msg |> Map.new() |> Map.put_new_lazy(:id, msg_id)
PlaywrightPort.post(state.port, msg)
{:noreply, Map.update!(state, :posts_in_flight, &Map.put(&1, msg.id, from))}
end
def handle_call({:received, guid}, _from, state) do
{:reply, Map.get(state.guid_received, guid, []), state}
end
def handle_call({:initializer, guid}, _from, state) do
{:reply, Map.get(state.initializers, guid), state}
end
def handle_call(:awaiting_started, from, %{status: :pending} = state) do
{:noreply, Map.update!(state, :awaiting_started, &[from | &1])}
end
def handle_call(:awaiting_started, _from, %{status: :started} = state) do
{:reply, :ok, state}
end
@impl GenServer
def handle_info({_, {:data, _}} = raw_msg, state) do
{port, msgs} = PlaywrightPort.parse(state.port, raw_msg)
state = %{state | port: port}
state = Enum.reduce(msgs, state, &handle_recv/2)
{:noreply, state}
end
defp handle_recv(msg, state) do
state
|> log_js_error(msg)
|> log_console(msg)
|> add_guid_ancestors(msg)
|> add_initializer(msg)
|> add_received(msg)
|> handle_started(msg)
|> reply_in_flight(msg)
|> notify_subscribers(msg)
end
defp log_js_error(state, %{method: "pageError"} = msg) do
Logger.error("Javascript error: #{inspect(msg.params.error)}")
state
end
defp log_js_error(state, _), do: state
defp log_console(state, %{method: "console"} = msg) do
config = Application.get_env(:phoenix_test, :playwright)
js_logger = Access.get(config, :js_logger, :default)
if js_logger == :default do
level =
case msg.params.type do
"error" -> :error
"debug" -> :debug
_ -> :info
end
Logger.log(level, "Javascript console: #{msg.params.text}")
end
state
end
defp log_console(state, _), do: state
defp handle_started(state, %{method: "__create__", params: %{type: "Playwright"}}) do
for from <- state.awaiting_started, do: GenServer.reply(from, :ok)
%{state | status: :started, awaiting_started: :none}
end
defp handle_started(state, _), do: state
defp add_guid_ancestors(state, %{method: "__create__"} = msg) do
child = msg.params.guid
parent = msg.guid
parent_ancestors = Map.get(state.guid_ancestors, parent, [])
Map.update!(state, :guid_ancestors, &Map.put(&1, child, [parent | parent_ancestors]))
end
defp add_guid_ancestors(state, _), do: state
defp add_initializer(state, %{method: "__create__"} = msg) do
Map.update!(state, :initializers, &Map.put(&1, msg.params.guid, msg.params.initializer))
end
defp add_initializer(state, _), do: state
defp reply_in_flight(%{posts_in_flight: in_flight} = state, msg)
when is_map_key(in_flight, msg.id) do
{from, in_flight} = Map.pop(in_flight, msg.id)
GenServer.reply(from, msg)
%{state | posts_in_flight: in_flight}
end
defp reply_in_flight(state, _), do: state
defp add_received(state, %{guid: guid} = msg) do
update_in(state.guid_received[guid], &[msg | &1 || []])
end
defp add_received(state, _), do: state
defp notify_subscribers(state, %{guid: guid} = msg) do
for guid <- [guid | Map.get(state.guid_ancestors, guid, [])],
pid <- Map.get(state.guid_subscribers, guid, []) do
send(pid, {:playwright, msg})
end
state
end
defp notify_subscribers(state, _), do: state
defp timeout do
Application.get_env(:phoenix_test, :timeout_ms, @default_timeout_ms)
end
end