Current section
Files
Jump to
Current section
Files
lib/phoenix_test/playwright/case.ex
setup_all_config_keys = ~w(browser headless slow_mo)a
setup_config_keys = ~w(screenshot trace)a
defmodule PhoenixTest.Playwright.Case do
@moduledoc """
ExUnit case module to assist with browser based tests.
`PhoenixTest.Playwright` and `PhoenixTest.Playwright.Config` explain
how to use and configure this module.
If the default setup behaviour and order does not suit you, consider
- using config opt `browser_context_opts`, which are passed to `PlaywrightEx.Browser.new_context/2`
- using config opt `browser_page_opts`, which are passed to `PlaywrightEx.BrowserContext.new_page/2`
- implementing your own `Case` (the setup functions in this module are public for your convenience)
"""
use ExUnit.CaseTemplate
alias Ecto.Adapters.SQL.Sandbox, as: EctoSandbox
alias Phoenix.Ecto.SQL.Sandbox, as: PhoenixSandbox
alias PhoenixTest.Playwright
alias PhoenixTest.Playwright.BrowserPool
alias PlaywrightEx.Browser
alias PlaywrightEx.BrowserContext
alias PlaywrightEx.Page
alias PlaywrightEx.Tracing
using opts do
quote do
import PhoenixTest
import PhoenixTest.Playwright,
# styler:sort
only: [
add_cookies: 2,
add_session_cookie: 3,
clear_cookies: 1,
clear_cookies: 2,
click: 2,
click: 3,
click: 4,
click_button: 4,
click_link: 4,
drag: 3,
evaluate: 2,
evaluate: 3,
evaluate: 4,
press: 3,
press: 4,
screenshot: 2,
screenshot: 3,
step: 3,
type: 3,
type: 4,
visit: 3,
with_dialog: 3
]
@moduletag Keyword.delete(unquote(opts), :async)
def timeout, do: PhoenixTest.Playwright.Config.global(:timeout)
end
end
setup_all :do_setup_all
setup :do_setup
@doc """
Merges the ExUnit context with `PhoenixTest.Playwright.Config` opts.
Uses the result to launch the browser.
Adds `:browser_id` to the context.
"""
def do_setup_all(context) do
keys = Playwright.Config.setup_all_keys()
config = context |> Map.take(keys) |> Playwright.Config.validate!() |> Keyword.take(keys)
if pool = config[:browser_pool] do
[browser_id: BrowserPool.checkout(pool)]
else
[browser_id: config |> Keyword.delete(:browser_pool) |> launch_browser()]
end
end
@doc """
Merges the ExUnit context with `PhoenixTest.Playwright.Config` opts.
Uses the result to create a new browser context and page.
Adds `:conn` to the context.
"""
def do_setup(context) do
config = context |> Map.take(Playwright.Config.setup_keys()) |> Playwright.Config.validate!()
[conn: new_session(config, context)]
end
defp launch_browser(config) do
timeout = Keyword.fetch!(config, :browser_launch_timeout)
browser = PhoenixTest.Playwright.Browser.launch_browser!(config)
on_exit(fn -> spawn(fn -> Browser.close(browser.guid, timeout: timeout) end) end)
browser.guid
end
def new_session(config, context) do
user_agent = checkout_ecto_repos(config, context)
base_url = Application.fetch_env!(:phoenix_test, :base_url)
context_opts_defaults = [base_url: base_url, locale: "en", user_agent: user_agent, timeout: config[:timeout]]
context_opts = Keyword.merge(context_opts_defaults, config[:browser_context_opts])
{:ok, browser_context} = Browser.new_context(context.browser_id, context_opts)
register_selector_engines!(browser_context.guid, config)
page_opts = Keyword.merge([timeout: config[:timeout]], config[:browser_page_opts])
{:ok, page} = BrowserContext.new_page(browser_context.guid, page_opts)
{:ok, _} = Page.update_subscription(page.guid, event: :console, enabled: true, timeout: config[:timeout])
{:ok, _} = Page.update_subscription(page.guid, event: :dialog, enabled: true, timeout: config[:timeout])
on_exit(fn -> spawn(fn -> BrowserContext.close(browser_context.guid, timeout: config[:timeout]) end) end)
if config[:trace], do: trace(browser_context.tracing.guid, config, context)
%{
context_id: browser_context.guid,
page_id: page.guid,
frame_id: page.main_frame.guid,
tracing_id: browser_context.tracing.guid,
config: config
}
|> Playwright.build()
|> tap(fn conn ->
if config[:screenshot], do: screenshot(conn, config, context)
end)
end
defp register_selector_engines!(browser_context_id, config) do
for {name, source} <- PhoenixTest.Playwright.Selector.Engines.custom() do
{:ok, _} =
BrowserContext.register_selector_engine(browser_context_id,
selector_engine: [name: to_string(name), source: source],
timeout: config[:timeout]
)
end
end
defp trace(tracing_id, config, context) do
opts = [screenshots: true, snapshots: true, sources: true, timeout: config[:timeout]]
{:ok, _} = Tracing.tracing_start(tracing_id, opts)
{:ok, _} = Tracing.tracing_start_chunk(tracing_id, timeout: config[:timeout])
File.mkdir_p!(config[:trace_dir])
file = Path.join(config[:trace_dir], file_name("_#{System.unique_integer([:positive, :monotonic])}.zip", context))
on_exit(fn ->
{:ok, zip_file} = Tracing.tracing_stop_chunk(tracing_id, timeout: config[:timeout])
{:ok, _} = Tracing.tracing_stop(tracing_id, timeout: config[:timeout])
File.cp!(zip_file.absolute_path, file)
maybe_open_trace(config[:trace], file)
end)
end
defp maybe_open_trace(:open, path) do
# Spawn to avoid blocking the test exit
spawn(fn -> System.cmd(Playwright.Config.executable(), ["show-trace", Path.absname(path)]) end)
:ok
end
defp maybe_open_trace(_, _), do: :ok
# Dialyzer warning suppressed: conn is opaque t() but closure captures struct
@dialyzer {:nowarn_function, screenshot: 3}
defp screenshot(conn, config, context) do
file = file_name(".png", context)
on_exit(fn ->
Playwright.screenshot(conn, file, config[:screenshot])
end)
end
defp file_name(suffix, %{module: module, test: test}) do
"Elixir." <> module = to_string(module)
time = :second |> :erlang.system_time() |> to_string()
String.replace("#{module}.#{test}_#{time}#{suffix}", ~r/[^a-zA-Z0-9\.]/, "_")
end
@includes_ecto Code.ensure_loaded?(EctoSandbox) && Code.ensure_loaded?(PhoenixSandbox)
if @includes_ecto do
defp checkout_ecto_repos(config, context) do
otp_app = Application.fetch_env!(:phoenix_test, :otp_app)
repos = Application.get_env(otp_app, :ecto_repos, [])
repos
|> Enum.map(&maybe_start_sandbox_owner(&1, config, context))
|> PhoenixSandbox.metadata_for(self())
|> PhoenixSandbox.encode_metadata()
end
defp maybe_start_sandbox_owner(repo, config, context) do
pid = EctoSandbox.start_owner!(repo, shared: not context.async)
on_exit(fn -> stop_sandbox_owner(pid, config, context) end)
repo
rescue
e in MatchError ->
case e do
%MatchError{term: {:error, {{:badmatch, {:already, :allowed}}, _}}} ->
# Already checked out (e.g. second new_session call in same test)
repo
_ ->
reraise e, __STACKTRACE__
end
end
defp stop_sandbox_owner(checkout_pid, config, context) do
if context.async do
spawn(fn -> do_stop_sandbox_owner(checkout_pid, config) end)
else
do_stop_sandbox_owner(checkout_pid, config)
end
end
defp do_stop_sandbox_owner(checkout_pid, config) do
delay = Keyword.fetch!(config, :ecto_sandbox_stop_owner_delay)
if delay > 0, do: Process.sleep(delay)
EctoSandbox.stop_owner(checkout_pid)
end
else
defp checkout_ecto_repos(_, _) do
"Ecto not loaded"
end
end
end