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 `PhoenixTest.Playwright.Browser.new_context/2` - using config opt `browser_page_opts`, which are passed to `PhoenixTest.Playwright.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 alias PhoenixTest.Playwright alias PhoenixTest.Playwright.Connection 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, press: 3, press: 4, screenshot: 2, screenshot: 3, type: 3, type: 4, with_dialog: 3 ] @moduletag Keyword.delete(unquote(opts), :async) 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) [browser_id: launch_browser(config)] 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 def launch_browser(opts) do Connection.ensure_started() {browser, opts} = Keyword.pop!(opts, :browser) browser_id = Connection.launch_browser(browser, opts) on_exit(fn -> Connection.post(guid: browser_id, method: :close) end) browser_id end def new_session(config, context) do browser_context_opts = Enum.into(config[:browser_context_opts], %{ locale: "en", user_agent: checkout_ecto_repos(context.async) || "No user agent" }) {:ok, browser_context_id} = Playwright.Browser.new_context(context.browser_id, browser_context_opts) register_selector_engines!(browser_context_id) {:ok, page_id} = Playwright.BrowserContext.new_page(browser_context_id, config[:browser_page_opts]) {:ok, _} = Playwright.Page.update_subscription(page_id, event: :console, enabled: true) {:ok, _} = Playwright.Page.update_subscription(page_id, event: :dialog, enabled: true) frame_id = Connection.initializer(page_id).main_frame.guid on_exit(fn -> Connection.post(guid: browser_context_id, method: :close) end) if config[:trace], do: trace(browser_context_id, config, context) if config[:screenshot], do: screenshot(page_id, config, context) Playwright.build(%{ context_id: browser_context_id, page_id: page_id, frame_id: frame_id, config: config }) end defp register_selector_engines!(browser_context_id) do for {name, source} <- Playwright.Selector.Engines.custom() do {:ok, _} = Playwright.BrowserContext.register_selector_engine(browser_context_id, to_string(name), source) end end defp trace(browser_context_id, config, context) do {:ok, _} = Playwright.BrowserContext.start_tracing(browser_context_id) File.mkdir_p!(config[:trace_dir]) file = file_name("_#{System.unique_integer([:positive, :monotonic])}.zip", context) path = Path.join(config[:trace_dir], file) on_exit(fn -> _ignore_error = Playwright.BrowserContext.stop_tracing(browser_context_id, path) if config[:trace] == :open do System.cmd( Playwright.Config.global(:runner), ["playwright", "show-trace", Path.join(File.cwd!(), path)], cd: Playwright.Config.global(:assets_dir) ) end end) end defp screenshot(page_id, config, context) do file = file_name(".png", context) on_exit(fn -> Playwright.screenshot(%{page_id: page_id}, 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?(Sandbox) && Code.ensure_loaded?(Phoenix.Ecto.SQL.Sandbox) if @includes_ecto do defp checkout_ecto_repos(async?) do otp_app = Application.fetch_env!(:phoenix_test, :otp_app) repos = Application.get_env(otp_app, :ecto_repos, []) repos |> Enum.map(&checkout_ecto_repo(&1, async?)) |> Phoenix.Ecto.SQL.Sandbox.metadata_for(self()) |> Phoenix.Ecto.SQL.Sandbox.encode_metadata() end defp checkout_ecto_repo(repo, async?) do case Sandbox.checkout(repo) do :ok -> :ok {:already, :allowed} -> :ok {:already, :owner} -> :ok end if not async?, do: Sandbox.mode(repo, {:shared, self()}) repo end else defp checkout_ecto_repos(_) do nil end end end