Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `PlaywrightEx.Frame.wait_for_selector/2`: `state` and `strict` options. #22, [@oliver-kriska]
- `PlaywrightEx.unsubscribe/2` and connection-level unsubscribe support.
- `PlaywrightEx.Frame.wait_for_load_state/2` and `PlaywrightEx.Frame.wait_for_url/2` with event-based navigation waiting.
- Per-frame event recorder process to keep waiter subscriptions continuous across waits.
- Per-frame resource process to keep navigation state and waiter subscriptions continuous across waits.
- `PlaywrightEx.Page.expect_url/2` for explicit URL expectations on pages.
- Regex support in argument serialization/deserialization using protocol-native `{r: %{p, f}}` values.
- `PlaywrightEx.Page.reload/2` to reload current page.
Expand Down
6 changes: 3 additions & 3 deletions lib/playwright_ex/channels/frame.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule PlaywrightEx.Frame do

alias PlaywrightEx.ChannelResponse
alias PlaywrightEx.Connection
alias PlaywrightEx.FrameEventRecorder
alias PlaywrightEx.Resource.Frame, as: FrameResource
alias PlaywrightEx.Serialization

schema =
Expand Down Expand Up @@ -1244,7 +1244,7 @@ defmodule PlaywrightEx.Frame do
{timeout, opts} = Keyword.pop!(opts, :timeout)
wait_state = opts |> Keyword.fetch!(:state) |> normalize_wait_state()

FrameEventRecorder.wait_for_load_state(connection, frame_id, wait_state, timeout)
FrameResource.await_load_state(connection, frame_id, wait_state, timeout)
end

schema =
Expand Down Expand Up @@ -1291,7 +1291,7 @@ defmodule PlaywrightEx.Frame do
wait_state = opts |> Keyword.fetch!(:wait_until) |> normalize_wait_state()
url_matcher = opts |> Keyword.fetch!(:url) |> build_url_matcher()

FrameEventRecorder.wait_for_url(connection, frame_id, url_matcher, wait_state, timeout)
FrameResource.await_url(connection, frame_id, url_matcher, wait_state, timeout)
end

defp normalize_wait_state(state) when is_atom(state), do: normalize_wait_state(Atom.to_string(state))
Expand Down
92 changes: 57 additions & 35 deletions lib/playwright_ex/processes/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule PlaywrightEx.Connection do

import Kernel, except: [send: 2]

alias PlaywrightEx.FrameEventRecorder
alias PlaywrightEx.Resource

@timeout_grace_factor 1.5
@min_genserver_timeout to_timeout(second: 1)
Expand Down Expand Up @@ -41,13 +41,23 @@ defmodule PlaywrightEx.Connection do
:gen_statem.cast(name, {:subscribe, pid, guid})
end

@doc false
def subscribe_sync(name, pid \\ self(), guid) do
:gen_statem.call(name, {:subscribe, pid, guid})
end

@doc """
Unsubscribe from messages for a guid.
"""
def unsubscribe(name, pid \\ self(), guid) do
:gen_statem.cast(name, {:unsubscribe, pid, guid})
end

@doc false
def unsubscribe_sync(name, pid \\ self(), guid) do
:gen_statem.call(name, {:unsubscribe, pid, guid})
end

@doc false
def handle_playwright_msg(name, msg) do
:gen_statem.cast(name, {:playwright_msg, msg})
Expand Down Expand Up @@ -83,6 +93,11 @@ defmodule PlaywrightEx.Connection do
:gen_statem.call(name, :remote?)
end

@doc false
def pg_scope(name) do
:gen_statem.call(name, :pg_scope)
end

# Internal

@impl :gen_statem
Expand All @@ -108,7 +123,7 @@ defmodule PlaywrightEx.Connection do

@doc false
def pending(:cast, {:playwright_msg, %{method: :__create__, params: %{guid: "Playwright"}} = msg}, data) do
{:next_state, :started, handle_create(data, msg)}
{:next_state, :started, cache_initializer(data, msg)}
end

def pending(:cast, _msg, _data), do: {:keep_state_and_data, [:postpone]}
Expand All @@ -129,20 +144,27 @@ defmodule PlaywrightEx.Connection do
{:keep_state_and_data, [{:reply, from, transport_module != PlaywrightEx.PortTransport}]}
end

def started(:cast, {:subscribe, recipient, guid}, data) do
group = pg_group(guid)
def started({:call, from}, :pg_scope, data) do
{:keep_state_and_data, [{:reply, from, data.config.pg_scope}]}
end

if recipient in :pg.get_members(data.config.pg_scope, group) do
:ok
else
:ok = :pg.join(data.config.pg_scope, group, recipient)
end
def started({:call, from}, {:subscribe, recipient, guid}, data) do
join_subscriber(data, recipient, guid)
{:keep_state_and_data, [{:reply, from, :ok}]}
end

def started({:call, from}, {:unsubscribe, recipient, guid}, data) do
leave_subscriber(data, recipient, guid)
{:keep_state_and_data, [{:reply, from, :ok}]}
end

def started(:cast, {:subscribe, recipient, guid}, data) do
join_subscriber(data, recipient, guid)
:keep_state_and_data
end

def started(:cast, {:unsubscribe, recipient, guid}, data) do
_ = :pg.leave(data.config.pg_scope, pg_group(guid), recipient)
leave_subscriber(data, recipient, guid)
:keep_state_and_data
end

Expand All @@ -151,7 +173,7 @@ defmodule PlaywrightEx.Connection do
module.log(:error, msg.params.error, msg)
end

:keep_state_and_data
{:keep_state, notify_subscribers(data, msg)}
end

def started(:cast, {:playwright_msg, %{method: :console} = msg}, data) do
Expand All @@ -160,7 +182,7 @@ defmodule PlaywrightEx.Connection do
module.log(level, msg.params.text, msg)
end

:keep_state_and_data
{:keep_state, notify_subscribers(data, msg)}
end

def started(:cast, {:playwright_msg, msg}, data) when is_map_key(data.pending_response, msg.id) do
Expand All @@ -171,38 +193,29 @@ defmodule PlaywrightEx.Connection do
end

def started(:cast, {:playwright_msg, msg}, data) do
{:keep_state,
data |> handle_create(msg) |> maybe_start_frame_event_recorder(msg) |> notify_subscribers(msg) |> handle_dispose(msg)}
end
data = cache_initializer(data, msg)
resource_context = %{connection: data.config.name, pg_scope: data.config.pg_scope}
Enum.each(Resource.modules(), & &1.maybe_start(resource_context, msg))
data = notify_subscribers(data, msg)

defp handle_create(data, %{method: :__create__} = msg) do
put_in(data.initializers[msg.params.guid], msg.params.initializer)
{:keep_state, release_disposed_guid(data, msg)}
end

defp handle_create(data, _msg), do: data

defp maybe_start_frame_event_recorder(data, %{
method: :__create__,
params: %{guid: guid, initializer: %{url: _url, load_states: _load_states} = initializer}
}) do
_ = FrameEventRecorder.ensure_started(data.config.name, guid, initializer)
data
defp cache_initializer(data, %{method: :__create__} = msg) do
put_in(data.initializers[msg.params.guid], msg.params.initializer)
end

defp maybe_start_frame_event_recorder(data, %{method: :__create__}) do
data
end
defp cache_initializer(data, _msg), do: data

defp maybe_start_frame_event_recorder(data, _msg), do: data
defp release_disposed_guid(data, %{method: :__dispose__} = msg) do
Enum.each(Resource.modules(), & &1.maybe_stop(data.config.name, msg.guid))

defp handle_dispose(data, %{method: :__dispose__} = msg) do
data
|> Map.update!(:initializers, &Map.delete(&1, msg.guid))
|> stop_disposed_frame_event_recorder(msg.guid)
|> clear_disposed_guid_subscribers(msg.guid)
end

defp handle_dispose(data, _msg), do: data
defp release_disposed_guid(data, _msg), do: data

defp notify_subscribers(data, %{guid: guid} = msg) do
for pid <- :pg.get_members(data.config.pg_scope, pg_group(guid)) do
Expand All @@ -226,9 +239,18 @@ defmodule PlaywrightEx.Connection do
data
end

defp stop_disposed_frame_event_recorder(data, guid) do
_ = FrameEventRecorder.terminate_frame(data.config.name, guid)
data
defp join_subscriber(data, recipient, guid) do
group = pg_group(guid)

if recipient in :pg.get_members(data.config.pg_scope, group) do
:ok
else
:ok = :pg.join(data.config.pg_scope, group, recipient)
end
end

defp leave_subscriber(data, recipient, guid) do
_ = :pg.leave(data.config.pg_scope, pg_group(guid), recipient)
end

defp log_level_from_js("error"), do: :error
Expand Down
130 changes: 130 additions & 0 deletions lib/playwright_ex/resource.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
defmodule PlaywrightEx.Resource do
@moduledoc false

alias PlaywrightEx.Resource.Frame
alias PlaywrightEx.Resource.Page

@type resource_module :: module()

@spec modules() :: [resource_module()]
def modules do
[Page, Frame]
end

@spec children(atom()) :: [Supervisor.child_spec()]
def children(connection) do
Enum.flat_map(modules(), fn module ->
[
{Registry, keys: :unique, name: module.registry_name(connection)},
{DynamicSupervisor, strategy: :one_for_one, name: module.supervisor_name(connection)}
]
end)
end

@spec ensure_started(resource_module(), atom(), PlaywrightEx.guid(), map() | nil, map()) ::
{:ok, pid()} | {:error, map()}
def ensure_started(module, connection, resource_id, initializer \\ nil, extra_opts \\ %{}) do
case lookup(module, connection, resource_id) do
{:ok, pid} ->
{:ok, pid}

:not_found ->
start_child(module, connection, resource_id, initializer, extra_opts)
end
end

@spec maybe_stop(resource_module(), atom(), PlaywrightEx.guid()) :: :ok
def maybe_stop(module, connection, resource_id) do
case lookup(module, connection, resource_id) do
{:ok, pid} -> Process.exit(pid, :normal)
:not_found -> :ok
end

:ok
end

@spec info(resource_module(), atom(), PlaywrightEx.guid()) :: map()
def info(module, connection, resource_id) do
call(module, connection, resource_id, :info)
end

@spec events(resource_module(), atom(), PlaywrightEx.guid(), pos_integer()) :: [map()]
def events(module, connection, resource_id, limit) do
call(module, connection, resource_id, {:events, limit})
end

@spec child_resources(resource_module(), atom(), PlaywrightEx.guid(), atom() | :all) :: map() | [map()]
def child_resources(module, connection, resource_id, type) do
call(module, connection, resource_id, {:child_resources, type})
end

@spec await(resource_module(), atom(), PlaywrightEx.guid(), term(), timeout()) :: any()
def await(module, connection, resource_id, request, timeout) do
with {:ok, pid} <- ensure_started(module, connection, resource_id) do
module.call_resource(pid, request, timeout)
end
end

@spec registry_name(resource_module(), atom()) :: atom()
def registry_name(module, connection) do
Module.concat(connection, "#{resource_name(module)}Registry")
end

@spec supervisor_name(resource_module(), atom()) :: atom()
def supervisor_name(module, connection) do
Module.concat(connection, "#{resource_name(module)}Supervisor")
end

@spec lookup(resource_module(), atom(), PlaywrightEx.guid()) :: {:ok, pid()} | :not_found
def lookup(module, connection, resource_id) do
registry = registry_name(module, connection)

case Registry.lookup(registry, resource_id) do
[{pid, _value}] -> {:ok, pid}
[] -> :not_found
end
rescue
ArgumentError -> :not_found
end

defp call(module, connection, resource_id, request) do
with {:ok, pid} <- ensure_started(module, connection, resource_id) do
GenServer.call(pid, request)
end
end

defp start_child(module, connection, resource_id, initializer, extra_opts) do
child_opts =
%{connection: connection, resource_id: resource_id}
|> maybe_put_initializer(initializer)
|> Map.merge(Map.take(extra_opts, [:pg_scope]))

child_spec = {module, child_opts}

case DynamicSupervisor.start_child(supervisor_name(module, connection), child_spec) do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
{:error, reason} -> {:error, %{message: "Failed to start #{resource_label(module)} resource: #{inspect(reason)}"}}
end
catch
:exit, reason ->
{:error, %{message: "Failed to start #{resource_label(module)} resource: #{Exception.format_exit(reason)}"}}
end

defp maybe_put_initializer(opts, initializer) when is_map(initializer), do: Map.put(opts, :initializer, initializer)
defp maybe_put_initializer(opts, _initializer), do: opts

defp resource_label(module) do
module
|> Module.split()
|> List.last()
|> Macro.underscore()
end

defp resource_name(module) do
module
|> Module.split()
|> Enum.drop_while(&(&1 != "Resource"))
|> Enum.join()
end
end
6 changes: 6 additions & 0 deletions lib/playwright_ex/resource/behaviour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
defmodule PlaywrightEx.Resource.Behaviour do
@moduledoc false

@callback maybe_start(map(), map()) :: :ok
@callback call_resource(pid(), term(), timeout()) :: any()
end
Loading