From 9b74cc8560a399db73c76817bb78c41fca343fe0 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Fri, 25 Oct 2024 08:23:43 +0200 Subject: [PATCH 1/6] Playwright driver, no deps --- .github/workflows/ci.yml | 9 + .gitignore | 6 + diff | 0 lib/phoenix_test.ex | 6 +- lib/phoenix_test/assertions.ex | 2 +- lib/phoenix_test/case.ex | 53 +++ lib/phoenix_test/driver.ex | 1 + lib/phoenix_test/live.ex | 2 + lib/phoenix_test/playwright.ex | 315 ++++++++++++++++++ lib/phoenix_test/playwright/connection.ex | 111 +++++++ lib/phoenix_test/playwright/frame.ex | 115 +++++++ lib/phoenix_test/playwright/message.ex | 41 +++ lib/phoenix_test/playwright/port.ex | 66 ++++ lib/phoenix_test/playwright/selector.ex | 38 +++ lib/phoenix_test/static.ex | 2 + priv/static/assets/app.css | 0 priv/static/assets/package-lock.json | 57 ++++ priv/static/assets/package.json | 6 + priv/static/driver.js | 1 + priv/static/favicon.ico | 0 test/phoenix_test/assertions_test.exs | 134 ++++---- test/phoenix_test/live_test.exs | 374 +++++++++++----------- test/phoenix_test/playwright_test.exs | 26 ++ test/phoenix_test/static_test.exs | 220 +++++++------ test/support/index_live.ex | 64 ++-- test/support/test_helpers.ex | 13 + test/test_helper.exs | 2 + 27 files changed, 1282 insertions(+), 382 deletions(-) create mode 100644 diff create mode 100644 lib/phoenix_test/case.ex create mode 100644 lib/phoenix_test/playwright.ex create mode 100644 lib/phoenix_test/playwright/connection.ex create mode 100644 lib/phoenix_test/playwright/frame.ex create mode 100644 lib/phoenix_test/playwright/message.ex create mode 100644 lib/phoenix_test/playwright/port.ex create mode 100644 lib/phoenix_test/playwright/selector.ex create mode 100644 priv/static/assets/app.css create mode 100644 priv/static/assets/package-lock.json create mode 100644 priv/static/assets/package.json create mode 120000 priv/static/driver.js create mode 100644 priv/static/favicon.ico create mode 100644 test/phoenix_test/playwright_test.exs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34366c57..6eff386c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,11 +63,20 @@ jobs: - name: Install dependencies run: mix deps.get + - name: Install JS dependencies + run: npm ci --prefix priv/static/assets + - name: Compiles without warnings run: mix compile --warnings-as-errors - name: Check Formatting run: mix format --check-formatted + - name: Build assets for browser tests + run: mix do assets.setup, assets.build + + - name: Install playwright browsers + run: npm exec --prefix priv/static/assets playwright install --with-deps + - name: Run tests run: mix test diff --git a/.gitignore b/.gitignore index 6bdf718f..727ba1a0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,9 @@ phoenix_test-*.tar # Ignore assets that are produced by build tools. /priv/static/assets/ + +!/priv/static/assets/app.css +!/priv/static/assets/package.json +!/priv/static/assets/package-lock.json + +.envrc diff --git a/diff b/diff new file mode 100644 index 00000000..e69de29b diff --git a/lib/phoenix_test.ex b/lib/phoenix_test.ex index afcf2bfb..6dda4dc8 100644 --- a/lib/phoenix_test.ex +++ b/lib/phoenix_test.ex @@ -213,7 +213,7 @@ defmodule PhoenixTest do LiveView or a static view. You don't need to worry about which type of page you're visiting. """ - def visit(conn, path) do + def visit(%Plug.Conn{} = conn, path) do case get(conn, path) do %{assigns: %{live_module: _}} = conn -> PhoenixTest.Live.build(conn) @@ -230,6 +230,10 @@ defmodule PhoenixTest do end end + def visit(driver, path) when is_struct(driver) do + Driver.visit(driver, path) + end + defp all_headers(conn) do Enum.map(conn.req_headers, &elem(&1, 0)) end diff --git a/lib/phoenix_test/assertions.ex b/lib/phoenix_test/assertions.ex index 60b6b104..b9cde11c 100644 --- a/lib/phoenix_test/assertions.ex +++ b/lib/phoenix_test/assertions.ex @@ -296,7 +296,7 @@ defmodule PhoenixTest.Assertions do |> append_found_other_matches(selector, other_matches) end - def refute_found_error_msg(selector, opts, found) do + defp refute_found_error_msg(selector, opts, found) do refute_count = Keyword.get(opts, :count, :any) at = Keyword.get(opts, :at, :any) text = Keyword.get(opts, :text, :no_text) diff --git a/lib/phoenix_test/case.ex b/lib/phoenix_test/case.ex new file mode 100644 index 00000000..f9ddc1ee --- /dev/null +++ b/lib/phoenix_test/case.ex @@ -0,0 +1,53 @@ +defmodule PhoenixTest.Case do + @moduledoc false + + use ExUnit.CaseTemplate + + alias PhoenixTest.Case + + @playwright_opts %{ + browser: :chromium, + headless: true, + slowMo: 0 + } + + setup_all context do + case context do + %{playwright: opts} -> + opts = Map.merge(@playwright_opts, if(opts == true, do: %{}, else: Map.new(opts))) + browser_id = Case.Playwright.launch_browser(opts) + [playwright: true, browser_id: browser_id] + + _ -> + :ok + end + end + + setup context do + case context do + %{playwright: false} -> [conn: Phoenix.ConnTest.build_conn()] + %{browser_id: browser_id} -> [conn: Case.Playwright.session(browser_id)] + end + end + + defmodule Playwright do + @moduledoc false + import PhoenixTest.Playwright.Connection + + def launch_browser(opts) do + ensure_started(opts) + browser_id = launch_browser(opts.browser, opts) + on_exit(fn -> sync_post(guid: browser_id, method: "close") end) + browser_id + end + + def session(browser_id) do + context_id = sync_post(guid: browser_id, method: "newContext").result.context.guid + page_id = sync_post(guid: context_id, method: "newPage").result.page.guid + [%{params: %{guid: "frame" <> _ = frame_id}}] = responses(page_id) + on_exit(fn -> post(guid: context_id, method: "close") end) + + PhoenixTest.Playwright.build(frame_id) + end + end +end diff --git a/lib/phoenix_test/driver.ex b/lib/phoenix_test/driver.ex index 59ddb17e..1d4cfcfd 100644 --- a/lib/phoenix_test/driver.ex +++ b/lib/phoenix_test/driver.ex @@ -1,5 +1,6 @@ defprotocol PhoenixTest.Driver do @moduledoc false + def visit(session, path) def render_page_title(session) def render_html(session) def click_link(session, selector, text) diff --git a/lib/phoenix_test/live.ex b/lib/phoenix_test/live.ex index 0b8f28fd..d2765651 100644 --- a/lib/phoenix_test/live.ex +++ b/lib/phoenix_test/live.ex @@ -367,6 +367,8 @@ defimpl PhoenixTest.Driver, for: PhoenixTest.Live do alias PhoenixTest.Assertions alias PhoenixTest.Live + def visit(_, _), do: raise(ArgumentError, message: "Unexpected: Call visit/1 with a %Plug.Conn{}.") + defdelegate render_page_title(session), to: Live defdelegate render_html(session), to: Live defdelegate click_link(session, selector, text), to: Live diff --git a/lib/phoenix_test/playwright.ex b/lib/phoenix_test/playwright.ex new file mode 100644 index 00000000..ae03aa34 --- /dev/null +++ b/lib/phoenix_test/playwright.ex @@ -0,0 +1,315 @@ +defmodule PhoenixTest.Playwright do + @moduledoc false + + alias PhoenixTest.Assertions + alias PhoenixTest.Element.Button + alias PhoenixTest.Element.Link + alias PhoenixTest.OpenBrowser + alias PhoenixTest.Playwright.Connection + alias PhoenixTest.Playwright.Frame + alias PhoenixTest.Playwright.Selector + alias PhoenixTest.Query + + defstruct [:frame_id, :last_input_selector, within: :none] + + @endpoint Application.compile_env(:phoenix_test, :endpoint) + @default_timeout :timer.seconds(1) + + def build(frame_id) do + %__MODULE__{frame_id: frame_id} + end + + def retry(fun, backoff_ms \\ [100, 250, 500, 1000]) + def retry(fun, []), do: fun.() + + def retry(fun, [sleep_ms | backoff_ms]) do + fun.() + rescue + ExUnit.AssertionError -> + Process.sleep(sleep_ms) + retry(fun, backoff_ms) + end + + def visit(session, path) do + base_url = Application.fetch_env!(:phoenix_test, :base_url) + Frame.goto(session.frame_id, base_url <> path) + session + end + + def assert_has(session, "title") do + retry(fn -> Assertions.assert_has(session, "title") end) + end + + def assert_has(session, selector), do: assert_has(session, selector, []) + + def assert_has(session, "title", opts) do + retry(fn -> Assertions.assert_has(session, "title", opts) end) + end + + def assert_has(session, selector, opts) do + unless found?(session, selector, opts) do + Assertions.assert_has(session, selector, opts) + end + + session + end + + def refute_has(session, "title") do + retry(fn -> Assertions.refute_has(session, "title") end) + end + + def refute_has(session, selector), do: refute_has(session, selector, []) + + def refute_has(session, "title", opts) do + retry(fn -> Assertions.refute_has(session, "title", opts) end) + end + + def refute_has(session, selector, opts) do + if found?(session, selector, opts) do + Assertions.refute_has(session, selector, opts) + end + + session + end + + defp found?(session, selector, opts) do + if opts[:count] && opts[:at] do + raise ArgumentError, message: "Options `count` and `at` can not be used together." + end + + selector = + session + |> maybe_within() + |> Selector.concat(Selector.css_or_locator(selector)) + |> Selector.concat(Selector.text(opts[:text], opts)) + |> Selector.concat(Selector.at(opts[:at])) + + if opts[:count] do + params = + %{ + expression: "to.have.count", + expectedNumber: opts[:count], + state: "attached", + isNot: false, + selector: selector, + timeout: timeout(opts) + } + + {:ok, found?} = Frame.expect(session.frame_id, params) + found? + else + params = + %{ + # Consistent with PhoenixTest: ignore visiblity + state: "attached", + selector: selector, + timeout: timeout(opts) + } + + case Frame.wait_for_selector(session.frame_id, params) do + {:ok, _} -> true + _ -> false + end + end + end + + def render_page_title(session) do + case Frame.title(session.frame_id) do + {:ok, ""} -> nil + {:ok, title} -> title + end + end + + def render_html(session) do + selector = maybe_within(session) + {:ok, html} = Frame.inner_html(session.frame_id, selector) + html + end + + def click_link(session, css_selector, text) do + selector = + session + |> maybe_within() + |> Selector.concat(Selector.css_or_locator(css_selector)) + |> Selector.concat(Selector.text(text, exact: false)) + + session.frame_id + |> Frame.click(selector, %{timeout: timeout()}) + |> handle_response(fn -> Link.find!(render_html(session), css_selector, text) end) + + session + end + + def click_button(session, css_selector, text) do + selector = + session + |> maybe_within() + |> Selector.concat(Selector.css_or_locator(css_selector)) + |> Selector.concat(Selector.text(text, exact: false)) + + session.frame_id + |> Frame.click(selector, %{timeout: timeout()}) + |> handle_response(fn -> Button.find!(render_html(session), css_selector, text) end) + + session + end + + def within(session, selector, fun) do + session + |> Map.put(:within, selector) + |> fun.() + |> Map.put(:within, :none) + end + + def fill_in(session, input_selector, label, opts) do + {value, opts} = Keyword.pop!(opts, :with) + fun = &Frame.fill(session.frame_id, &1, to_string(value), &2) + input(session, input_selector, label, opts, fun) + end + + def select(session, input_selector, option_labels, opts) do + # TODO Support exact_option + if opts[:exact_option] != true, do: raise("exact_option not implemented") + + {label, opts} = Keyword.pop!(opts, :from) + options = option_labels |> List.wrap() |> Enum.map(&%{label: &1}) + fun = &Frame.select_option(session.frame_id, &1, options, &2) + input(session, input_selector, label, opts, fun) + end + + def check(session, input_selector, label, opts) do + fun = &Frame.check(session.frame_id, &1, &2) + input(session, input_selector, label, opts, fun) + end + + def uncheck(session, input_selector, label, opts) do + fun = &Frame.uncheck(session.frame_id, &1, &2) + input(session, input_selector, label, opts, fun) + end + + def choose(session, input_selector, label, opts) do + fun = &Frame.check(session.frame_id, &1, &2) + input(session, input_selector, label, opts, fun) + end + + def upload(session, input_selector, label, paths, opts) do + paths = paths |> List.wrap() |> Enum.map(&Path.expand/1) + fun = &Frame.set_input_files(session.frame_id, &1, paths, &2) + input(session, input_selector, label, opts, fun) + end + + defp input(session, input_selector, label, opts, fun) do + selector = + session + |> maybe_within() + |> Selector.concat(Selector.css_or_locator(input_selector)) + |> Selector.and(Selector.label(label, opts)) + + selector + |> fun.(%{timeout: timeout(opts)}) + |> handle_response(fn -> Query.find_by_label!(render_html(session), input_selector, label, opts) end) + + %{session | last_input_selector: selector} + end + + defp maybe_within(session) do + case session.within do + :none -> "*" + selector -> "css=#{selector}" + end + end + + defp handle_response(result, error_fun) do + case result do + {:error, %{name: "TimeoutError"}} -> + error_fun.() + raise ExUnit.AssertionError, message: "Could not find element." + + {:error, %{name: "Error", message: "Error: strict mode violation" <> _}} -> + error_fun.() + raise ExUnit.AssertionError, message: "Found more than one element." + + {:error, %{name: "Error", message: "Clicking the checkbox did not change its state"}} -> + :ok + + {:ok, result} -> + result + end + end + + def submit(session) do + Frame.press(session.frame_id, session.last_input_selector, "Enter") + session + end + + def open_browser(session, open_fun \\ &OpenBrowser.open_with_system_cmd/1) do + {:ok, html} = Frame.content(session.frame_id) + + fixed_html = + html + |> Floki.parse_document!() + |> Floki.traverse_and_update(&OpenBrowser.prefix_static_paths(&1, @endpoint)) + |> Floki.raw_html() + + path = Path.join([System.tmp_dir!(), "phx-test#{System.unique_integer([:monotonic])}.html"]) + File.write!(path, fixed_html) + open_fun.(path) + + session + end + + def unwrap(session, fun) do + fun.(session.frame_id) + session + end + + def current_path(session) do + resp = + session.frame_id + |> Connection.responses() + |> Enum.find(&match?(%{method: "navigated", params: %{url: _}}, &1)) + + if resp == nil, do: raise(ArgumentError, "Could not find current path.") + + uri = URI.parse(resp.params.url) + [uri.path, uri.query] |> Enum.reject(&is_nil/1) |> Enum.join("?") + end + + defp timeout(opts \\ []) do + default = Application.get_env(:phoenix_test, :timeout, @default_timeout) + Keyword.get(opts, :timeout, default) + end +end + +defimpl PhoenixTest.Driver, for: PhoenixTest.Playwright do + alias PhoenixTest.Assertions + alias PhoenixTest.Playwright + + defdelegate visit(session, path), to: Playwright + defdelegate render_page_title(session), to: Playwright + defdelegate render_html(session), to: Playwright + defdelegate click_link(session, selector, text), to: Playwright + defdelegate click_button(session, selector, text), to: Playwright + defdelegate within(session, selector, fun), to: Playwright + defdelegate fill_in(session, input_selector, label, opts), to: Playwright + defdelegate select(session, input_selector, option, opts), to: Playwright + defdelegate check(session, input_selector, label, opts), to: Playwright + defdelegate uncheck(session, input_selector, label, opts), to: Playwright + defdelegate choose(session, input_selector, label, opts), to: Playwright + defdelegate upload(session, input_selector, label, path, opts), to: Playwright + defdelegate submit(session), to: Playwright + defdelegate open_browser(session), to: Playwright + defdelegate open_browser(session, open_fun), to: Playwright + defdelegate unwrap(session, fun), to: Playwright + defdelegate current_path(session), to: Playwright + + defdelegate assert_has(session, selector), to: Playwright + defdelegate assert_has(session, selector, opts), to: Playwright + defdelegate refute_has(session, selector), to: Playwright + defdelegate refute_has(session, selector, opts), to: Playwright + + def assert_path(session, path), do: Playwright.retry(fn -> Assertions.assert_path(session, path) end) + def assert_path(session, path, opts), do: Playwright.retry(fn -> Assertions.assert_path(session, path, opts) end) + def refute_path(session, path), do: Playwright.retry(fn -> Assertions.refute_path(session, path) end) + def refute_path(session, path, opts), do: Playwright.retry(fn -> Assertions.refute_path(session, path, opts) end) +end diff --git a/lib/phoenix_test/playwright/connection.ex b/lib/phoenix_test/playwright/connection.ex new file mode 100644 index 00000000..c2fdead8 --- /dev/null +++ b/lib/phoenix_test/playwright/connection.ex @@ -0,0 +1,111 @@ +defmodule PhoenixTest.Playwright.Connection do + @moduledoc false + use GenServer + + alias PhoenixTest.Playwright.Port, as: PlaywrightPort + + defstruct [ + :port, + :init, + responses: %{}, + pending_init: [], + pending_response: %{} + ] + + def start_link(config) do + GenServer.start_link(__MODULE__, config, name: __MODULE__, timeout: 1000) + end + + def ensure_started(name \\ __MODULE__, config) do + case Process.whereis(name) do + nil -> start_link(config) + pid -> {:ok, pid} + end + end + + def launch_browser(name \\ __MODULE__, type, opts) do + type_id = GenServer.call(name, {:browser_type_id, type}) + resp = sync_post(guid: type_id, method: "launch", params: Map.new(opts)) + resp.result.browser.guid + end + + def post(name \\ __MODULE__, msg) do + GenServer.cast(name, {:post, msg}) + end + + def sync_post(name \\ __MODULE__, msg) do + GenServer.call(name, {:sync_post, msg}) + end + + def responses(name \\ __MODULE__, guid) do + GenServer.call(name, {:responses, guid}) + end + + @impl GenServer + def init(config) do + port = PlaywrightPort.open(config) + msg = %{guid: "", params: %{sdkLanguage: "javascript"}, method: "initialize"} + PlaywrightPort.post(port, msg) + + {:ok, %__MODULE__{port: port}} + end + + @impl GenServer + def handle_cast({:post, msg}, state) do + PlaywrightPort.post(state.port, msg) + {:noreply, state} + end + + @impl GenServer + def handle_call({:sync_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, :pending_response, &Map.put(&1, msg.id, from))} + end + + def handle_call({:responses, guid}, _from, state) do + {:reply, Map.get(state.responses, guid, []), state} + end + + def handle_call({:browser_type_id, type}, from, %{init: nil} = state) do + fun = &GenServer.reply(from, browser_type_id(&1, type)) + {:noreply, Map.update!(state, :pending_init, &[fun | &1])} + end + + def handle_call({:browser_type_id, type}, _from, state) do + {:reply, browser_type_id(state.init, type), 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(%{params: %{type: "Playwright"}} = msg, state) do + init = msg.params.initializer + for fun <- state.pending_init, do: fun.(init) + + %{state | init: init, pending_init: :done} + end + + defp handle_recv(msg, %{pending_response: pending} = state) when is_map_key(pending, msg.id) do + {from, pending} = Map.pop(pending, msg.id) + GenServer.reply(from, msg) + + %{state | pending_response: pending} + end + + defp handle_recv(%{guid: guid} = msg, state) do + update_in(state.responses[guid], &[msg | &1 || []]) + end + + defp handle_recv(_msg, state), do: state + + defp browser_type_id(init, type), do: Map.fetch!(init, type).guid +end diff --git a/lib/phoenix_test/playwright/frame.ex b/lib/phoenix_test/playwright/frame.ex new file mode 100644 index 00000000..e985ded1 --- /dev/null +++ b/lib/phoenix_test/playwright/frame.ex @@ -0,0 +1,115 @@ +defmodule PhoenixTest.Playwright.Frame do + @moduledoc false + import PhoenixTest.Playwright.Connection, only: [sync_post: 1] + + def goto(frame_id, url) do + params = %{url: url} + sync_post(guid: frame_id, method: :goto, params: params) + :ok + end + + def url(frame_id) do + [guid: frame_id, method: :url, params: %{}] + |> sync_post() + |> unwrap_response(& &1.result.value) + end + + def press(frame_id, selector, key) do + params = %{selector: selector, key: key} + sync_post(guid: frame_id, method: :press, params: params) + :ok + end + + def title(frame_id) do + [guid: frame_id, method: :title] + |> sync_post() + |> unwrap_response(& &1.result.value) + end + + def expect(frame_id, params) do + [guid: frame_id, method: :expect, params: params] + |> sync_post() + |> unwrap_response(& &1.result.matches) + end + + def wait_for_selector(frame_id, params) do + [guid: frame_id, method: :waitForSelector, params: params] + |> sync_post() + |> unwrap_response(& &1.result.element) + end + + def inner_html(frame_id, selector) do + params = %{selector: selector} + + [guid: frame_id, method: :innerHTML, params: params] + |> sync_post() + |> unwrap_response(& &1.result.value) + end + + def content(frame_id) do + [guid: frame_id, method: :content] + |> sync_post() + |> unwrap_response(& &1.result.value) + end + + def fill(frame_id, selector, value, opts \\ []) do + params = %{selector: selector, value: value, strict: true} + params = Enum.into(opts, params) + + [guid: frame_id, method: :fill, params: params] + |> sync_post() + |> unwrap_response(& &1) + end + + def select_option(frame_id, selector, options, opts \\ []) do + params = %{selector: selector, options: options, strict: true} + params = Enum.into(opts, params) + + [guid: frame_id, method: :selectOption, params: params] + |> sync_post() + |> unwrap_response(& &1) + end + + def check(frame_id, selector, opts \\ []) do + params = %{selector: selector, strict: true} + params = Enum.into(opts, params) + + [guid: frame_id, method: :check, params: params] + |> sync_post() + |> unwrap_response(& &1) + end + + def uncheck(frame_id, selector, opts \\ []) do + params = %{selector: selector, strict: true} + params = Enum.into(opts, params) + + [guid: frame_id, method: :uncheck, params: params] + |> sync_post() + |> unwrap_response(& &1) + end + + def set_input_files(frame_id, selector, paths, opts \\ []) do + params = %{selector: selector, localPaths: paths, strict: true} + params = Enum.into(opts, params) + + [guid: frame_id, method: :setInputFiles, params: params] + |> sync_post() + |> unwrap_response(& &1) + end + + def click(frame_id, selector, opts \\ []) do + params = %{selector: selector, waitUntil: "load", strict: true} + params = Enum.into(opts, params) + + [guid: frame_id, method: :click, params: params] + |> sync_post() + |> unwrap_response(& &1) + end + + defp unwrap_response(response, fun) do + case response do + %{error: %{error: error}} -> {:error, error} + _ -> {:ok, fun.(response)} + end + end +end diff --git a/lib/phoenix_test/playwright/message.ex b/lib/phoenix_test/playwright/message.ex new file mode 100644 index 00000000..84b344d6 --- /dev/null +++ b/lib/phoenix_test/playwright/message.ex @@ -0,0 +1,41 @@ +defmodule PhoenixTest.Playwright.Message do + @moduledoc false + + def parse(<>, 0, "", accumulated) do + %{ + buffer: "", + frames: accumulated, + remaining: head + } + end + + def parse(<>, 0, "", accumulated) do + parse(data, head, "", accumulated) + end + + def parse(<>, read_length, buffer, accumulated) when byte_size(data) == read_length do + %{ + buffer: "", + frames: accumulated ++ [buffer <> data], + remaining: 0 + } + end + + def parse(<>, read_length, buffer, accumulated) when byte_size(data) > read_length do + {message, tail} = bytewise_split(data, read_length) + parse(tail, 0, "", accumulated ++ [buffer <> message]) + end + + def parse(<>, read_length, buffer, accumulated) when byte_size(data) < read_length do + %{ + buffer: buffer <> data, + frames: accumulated, + remaining: read_length - byte_size(data) + } + end + + defp bytewise_split(input, offset) do + <> = input + {head, tail} + end +end diff --git a/lib/phoenix_test/playwright/port.ex b/lib/phoenix_test/playwright/port.ex new file mode 100644 index 00000000..053bcf23 --- /dev/null +++ b/lib/phoenix_test/playwright/port.ex @@ -0,0 +1,66 @@ +defmodule PhoenixTest.Playwright.Port do + @moduledoc false + + alias PhoenixTest.Playwright.Message + + defstruct [ + :port, + :remaining, + :buffer + ] + + def open(config) do + cli = Map.get(config, :driver_path, default_cli()) + cmd = "run-driver" + port = Port.open({:spawn, "#{cli} #{cmd}"}, [:binary]) + + %__MODULE__{port: port, remaining: 0, buffer: ""} + end + + def post(state, msg) do + default = %{params: %{}, metadata: %{}} + frame = msg |> Enum.into(default) |> serialize() + length = byte_size(frame) + padding = <> + Port.command(state.port, padding <> frame) + end + + def parse(%{port: port} = state, {port, {:data, data}}) do + parsed = Message.parse(data, state.remaining, state.buffer, []) + %{frames: frames, buffer: buffer, remaining: remaining} = parsed + state = %{state | buffer: buffer, remaining: remaining} + msgs = Enum.map(frames, &deserialize/1) + + {state, msgs} + end + + defp default_cli do + Path.join(:code.priv_dir(:phoenix_test), "static/driver.js") + end + + defp deserialize(json) do + case Jason.decode(json) do + {:ok, data} -> atom_keys(data) + error -> decode_error(json, error) + end + end + + defp decode_error(json, error) do + msg = "error: #{inspect(error)}; #{inspect(json: Enum.join(for <>, do: <>))}" + raise ArgumentError, message: msg + end + + defp serialize(message) do + Jason.encode!(message) + end + + defp atom_keys(map) when is_map(map) do + Map.new(map, fn + {k, v} when is_map(v) -> {String.to_atom(k), atom_keys(v)} + {k, list} when is_list(list) -> {String.to_atom(k), Enum.map(list, fn v -> atom_keys(v) end)} + {k, v} -> {String.to_atom(k), v} + end) + end + + defp atom_keys(other), do: other +end diff --git a/lib/phoenix_test/playwright/selector.ex b/lib/phoenix_test/playwright/selector.ex new file mode 100644 index 00000000..1c97590e --- /dev/null +++ b/lib/phoenix_test/playwright/selector.ex @@ -0,0 +1,38 @@ +defmodule PhoenixTest.Playwright.Selector do + @moduledoc false + + def concat(left, :none), do: left + def concat(left, right), do: "#{left} >> #{right}" + + def unquote(:and)(left, :none), do: left + def unquote(:and)(left, right), do: concat(left, "internal:and=#{Jason.encode!(right)}") + defdelegate _and(left, right), to: __MODULE__, as: :and + + def text(nil, _opts), do: :none + def text(text, opts), do: "internal:text=\"#{text}\"#{exact_suffix(opts)}" + + def label(nil, _opts), do: :none + def label(label, opts), do: "internal:label=\"#{label}\"#{exact_suffix(opts)}" + + def at(nil), do: :none + def at(at), do: "nth=#{at}" + + def css_or_locator(nil), do: :none + def css_or_locator([]), do: :none + def css_or_locator(selector) when is_binary(selector), do: css_or_locator([selector]) + def css_or_locator(selectors) when is_list(selectors), do: "css=#{Enum.join(selectors, ",")}" + + def css_or_locator(%PhoenixTest.Locators.Input{} = input) do + attrs = + input + |> Map.take(~w(type value)a) + |> Enum.reject(fn {_, v} -> is_nil(v) end) + |> Enum.map_join("", fn {k, v} -> "[#{k}='#{v}']" end) + + input.label |> label(exact: true) |> _and(css_or_locator(attrs)) + end + + defp exact_suffix(opts) when is_list(opts), do: opts |> Keyword.get(:exact, false) |> exact_suffix() + defp exact_suffix(true), do: "s" + defp exact_suffix(false), do: "i" +end diff --git a/lib/phoenix_test/static.ex b/lib/phoenix_test/static.ex index 01cdc089..67a35fea 100644 --- a/lib/phoenix_test/static.ex +++ b/lib/phoenix_test/static.ex @@ -292,6 +292,8 @@ defimpl PhoenixTest.Driver, for: PhoenixTest.Static do alias PhoenixTest.Assertions alias PhoenixTest.Static + def visit(_, _), do: raise(ArgumentError, message: "Unexpected: Call visit/1 with a %Plug.Conn{}.") + defdelegate render_page_title(session), to: Static defdelegate render_html(session), to: Static defdelegate click_link(session, selector, text), to: Static diff --git a/priv/static/assets/app.css b/priv/static/assets/app.css new file mode 100644 index 00000000..e69de29b diff --git a/priv/static/assets/package-lock.json b/priv/static/assets/package-lock.json new file mode 100644 index 00000000..31a9962e --- /dev/null +++ b/priv/static/assets/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "phoenix_test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "phoenix_test", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "playwright": "^1.48.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", + "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", + "dependencies": { + "playwright-core": "1.48.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", + "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/priv/static/assets/package.json b/priv/static/assets/package.json new file mode 100644 index 00000000..c3d2d5ec --- /dev/null +++ b/priv/static/assets/package.json @@ -0,0 +1,6 @@ +{ + "name": "phoenix_test", + "dependencies": { + "playwright": "^1.48.1" + } +} diff --git a/priv/static/driver.js b/priv/static/driver.js new file mode 120000 index 00000000..4f62e658 --- /dev/null +++ b/priv/static/driver.js @@ -0,0 +1 @@ +assets/node_modules/playwright/cli.js \ No newline at end of file diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/test/phoenix_test/assertions_test.exs b/test/phoenix_test/assertions_test.exs index c11d8025..5ebdcae2 100644 --- a/test/phoenix_test/assertions_test.exs +++ b/test/phoenix_test/assertions_test.exs @@ -1,5 +1,5 @@ defmodule PhoenixTest.AssertionsTest do - use ExUnit.Case, async: true + use PhoenixTest.Case, async: true import PhoenixTest import PhoenixTest.Locators @@ -8,18 +8,16 @@ defmodule PhoenixTest.AssertionsTest do alias ExUnit.AssertionError alias PhoenixTest.Live - setup do - %{conn: Phoenix.ConnTest.build_conn()} - end + @moduletag :playwright describe "assert_has/2" do - test "succeeds if single element is found with CSS selector", %{conn: conn} do + also_test_js "succeeds if single element is found with CSS selector", %{conn: conn} do conn |> visit("/page/index") |> assert_has("[data-role='title']") end - test "raises an error if the element cannot be found at all", %{conn: conn} do + also_test_js "raises an error if the element cannot be found at all", %{conn: conn} do conn = visit(conn, "/page/index") msg = ~r/Could not find any elements with selector "#nonexistent-id"/ @@ -29,25 +27,25 @@ defmodule PhoenixTest.AssertionsTest do end end - test "succeeds if element searched is title (Static)", %{conn: conn} do + also_test_js "succeeds if element searched is title (Static)", %{conn: conn} do conn |> visit("/page/index") |> assert_has("title") end - test "succeeds if element searched is title (Live)", %{conn: conn} do + also_test_js "succeeds if element searched is title (Live)", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title") end - test "succeeds if more than one element matches selector", %{conn: conn} do + also_test_js "succeeds if more than one element matches selector", %{conn: conn} do conn |> visit("/page/index") |> assert_has("li") end - test "takes in input helper in assertion", %{conn: conn} do + also_test_js "takes in input helper in assertion", %{conn: conn} do conn |> visit("/page/index") |> assert_has(input(type: "text", label: "User Name")) @@ -55,7 +53,7 @@ defmodule PhoenixTest.AssertionsTest do end describe "assert_has/3" do - test "succeeds if single element is found with CSS selector and text (Static)", %{conn: conn} do + also_test_js "succeeds if single element is found with CSS selector and text (Static)", %{conn: conn} do conn |> visit("/page/index") |> assert_has("h1", text: "Main page") @@ -64,7 +62,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("[data-role='title']", text: "Main page") end - test "succeeds if single element is found with CSS selector and text (Live)", %{conn: conn} do + also_test_js "succeeds if single element is found with CSS selector and text (Live)", %{conn: conn} do conn |> visit("/live/index") |> assert_has("h1", text: "LiveView main page") @@ -73,7 +71,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("[data-role='title']", text: "LiveView main page") end - test "succeeds if more than one element matches selector but text narrows it down", %{ + also_test_js "succeeds if more than one element matches selector but text narrows it down", %{ conn: conn } do conn @@ -81,25 +79,25 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("li", text: "Aragorn") end - test "succeeds if more than one element matches selector and text", %{conn: conn} do + also_test_js "succeeds if more than one element matches selector and text", %{conn: conn} do conn |> visit("/page/index") |> assert_has(".multiple_links", text: "Multiple links") end - test "succeeds if text difference is only a matter of truncation", %{conn: conn} do + also_test_js "succeeds if text difference is only a matter of truncation", %{conn: conn} do conn |> visit("/page/index") |> assert_has(".has_extra_space", text: "Has extra space") end - test "succeeds when a non-200 status code is returned", %{conn: conn} do + also_test_js "succeeds when a non-200 status code is returned", %{conn: conn} do conn |> visit("/page/unauthorized") |> assert_has("h1", text: "Unauthorized") end - test "raises an error if the element cannot be found at all", %{conn: conn} do + also_test_js "raises an error if the element cannot be found at all", %{conn: conn} do conn = visit(conn, "/page/index") msg = ~r/Could not find any elements with selector "#nonexistent-id"/ @@ -109,7 +107,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises error if element cannot be found but selector matches other elements", %{ + also_test_js "raises error if element cannot be found but selector matches other elements", %{ conn: conn } do conn = visit(conn, "/page/index") @@ -130,25 +128,25 @@ defmodule PhoenixTest.AssertionsTest do end end - test "can be used to assert on page title (Static)", %{conn: conn} do + also_test_js "can be used to assert on page title (Static)", %{conn: conn} do conn |> visit("/page/index") |> assert_has("title", text: "PhoenixTest is the best!") end - test "can be used to assert on page title (Live)", %{conn: conn} do + also_test_js "can be used to assert on page title (Live)", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title", text: "PhoenixTest is the best!") end - test "can assert title's exactness", %{conn: conn} do + also_test_js "can assert title's exactness", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title", text: "PhoenixTest is the best!", exact: true) end - test "raises if title does not match expected value (Static)", %{conn: conn} do + also_test_js "raises if title does not match expected value (Static)", %{conn: conn} do msg = ignore_whitespace(""" Expected title to be "Not the title" but got "PhoenixTest is the best!" @@ -161,7 +159,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises if title does not match expected value (Live)", %{conn: conn} do + also_test_js "raises if title does not match expected value (Live)", %{conn: conn} do msg = ignore_whitespace(""" Expected title to be "Not the title" but got "PhoenixTest is the best!" @@ -174,8 +172,8 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises if title is contained but is not exactly the same as expected (with exact=true)", - %{conn: conn} do + also_test_js "raises if title is contained but is not exactly the same as expected (with exact=true)", + %{conn: conn} do msg = ignore_whitespace(""" Expected title to be "PhoenixTest" but got "PhoenixTest is the best!" @@ -188,7 +186,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises error if element cannot be found and selector matches a nested structure", %{ + also_test_js "raises error if element cannot be found and selector matches a nested structure", %{ conn: conn } do conn = visit(conn, "/page/index") @@ -217,7 +215,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "accepts a `count` option", %{conn: conn} do + also_test_js "accepts a `count` option", %{conn: conn} do conn |> visit("/page/index") |> assert_has(".multiple_links", count: 2) @@ -226,7 +224,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("h1", text: "Main page", count: 1) end - test "raises an error if count is more than expected count", %{conn: conn} do + also_test_js "raises an error if count is more than expected count", %{conn: conn} do session = visit(conn, "/page/index") msg = @@ -241,7 +239,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises an error if count is less than expected count", %{conn: conn} do + also_test_js "raises an error if count is less than expected count", %{conn: conn} do session = visit(conn, "/page/index") msg = @@ -256,14 +254,14 @@ defmodule PhoenixTest.AssertionsTest do end end - test "accepts an `exact` option to match text exactly", %{conn: conn} do + also_test_js "accepts an `exact` option to match text exactly", %{conn: conn} do conn |> visit("/page/index") |> assert_has("h1", text: "Main", exact: false) |> assert_has("h1", text: "Main page", exact: true) end - test "raises if `exact` text doesn't match", %{conn: conn} do + also_test_js "raises if `exact` text doesn't match", %{conn: conn} do msg = ignore_whitespace(""" Could not find any elements with selector "h1" and text "Main". @@ -282,13 +280,17 @@ defmodule PhoenixTest.AssertionsTest do end end + # Different semantics: + # - PhoenixTest: Assert second li has text is "Legolas" + # - Playwright: Assert two li with text "Legolas" exist + @tag playwright: false, reason: :known_inconsistency test "accepts an `at` option to assert on a specific element", %{conn: conn} do conn |> visit("/page/index") |> assert_has("#multiple-items li", at: 2, text: "Legolas") end - test "raises if it cannot find element at `at` position", %{conn: conn} do + also_test_js "raises if it cannot find element at `at` position", %{conn: conn} do msg = ignore_whitespace(""" Could not find any elements with selector "#multiple-items li" and text "Aragorn" at position 2 @@ -303,28 +305,28 @@ defmodule PhoenixTest.AssertionsTest do end describe "refute_has/2" do - test "succeeds if no element is found with CSS selector (Static)", %{conn: conn} do + also_test_js "succeeds if no element is found with CSS selector (Static)", %{conn: conn} do conn |> visit("/page/index") |> refute_has("#some-invalid-id") |> refute_has("[data-role='invalid-role']") end - test "succeeds if no element is found with CSS selector (Live)", %{conn: conn} do + also_test_js "succeeds if no element is found with CSS selector (Live)", %{conn: conn} do conn |> visit("/live/index") |> refute_has("#some-invalid-id") |> refute_has("[data-role='invalid-role']") end - test "can refute presence of title (Static)", %{conn: conn} do + also_test_js "can refute presence of title (Static)", %{conn: conn} do conn |> visit("/page/index_no_layout") |> refute_has("title") |> refute_has("#something-else-to-test-pipe") end - test "accepts a `count` option", %{conn: conn} do + also_test_js "accepts a `count` option", %{conn: conn} do conn |> visit("/page/index") |> refute_has("h1", count: 2) @@ -333,7 +335,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has(".multiple_links", text: "Multiple links", count: 1) end - test "raises if element is found", %{conn: conn} do + also_test_js "raises if element is found", %{conn: conn} do msg = ignore_whitespace(""" Expected not to find any elements with selector "h1". @@ -352,7 +354,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises if title is found", %{conn: conn} do + also_test_js "raises if title is found", %{conn: conn} do msg = ignore_whitespace(""" Expected title not to be present but found: "PhoenixTest is the best!" @@ -365,7 +367,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises an error if multiple elements are found", %{conn: conn} do + also_test_js "raises an error if multiple elements are found", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -380,7 +382,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises if there is one element and count is 1", %{conn: conn} do + also_test_js "raises if there is one element and count is 1", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -393,7 +395,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises if there are the same number of elements as refuted", %{conn: conn} do + also_test_js "raises if there are the same number of elements as refuted", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -410,27 +412,27 @@ defmodule PhoenixTest.AssertionsTest do end describe "refute_has/3" do - test "can be used to refute on page title (Static)", %{conn: conn} do + also_test_js "can be used to refute on page title (Static)", %{conn: conn} do conn |> visit("/page/index") |> refute_has("title", text: "Not the title") |> refute_has("title", text: "Not this title either") end - test "can be used to refute on page title (Live)", %{conn: conn} do + also_test_js "can be used to refute on page title (Live)", %{conn: conn} do conn |> visit("/live/index") |> refute_has("title", text: "Not the title") |> refute_has("title", text: "Not this title either") end - test "can be used to refute page title's exactness", %{conn: conn} do + also_test_js "can be used to refute page title's exactness", %{conn: conn} do conn |> visit("/live/index") |> refute_has("title", text: "PhoenixTest is the", exact: true) end - test "raises if title matches value (Static)", %{conn: conn} do + also_test_js "raises if title matches value (Static)", %{conn: conn} do msg = ignore_whitespace(""" Expected title not to be "PhoenixTest is the best!" @@ -443,7 +445,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises if title matches value (Live)", %{conn: conn} do + also_test_js "raises if title matches value (Live)", %{conn: conn} do msg = ignore_whitespace(""" Expected title not to be "PhoenixTest is the best!" @@ -456,7 +458,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "succeeds if no element is found with CSS selector and text (Static)", %{conn: conn} do + also_test_js "succeeds if no element is found with CSS selector and text (Static)", %{conn: conn} do conn |> visit("/page/index") |> refute_has("h1", text: "Not main page") @@ -465,7 +467,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has("#title", text: "Not main page") end - test "succeeds if no element is found with CSS selector and text (Live)", %{conn: conn} do + also_test_js "succeeds if no element is found with CSS selector and text (Live)", %{conn: conn} do conn |> visit("/live/index") |> refute_has("h1", text: "Not main page") @@ -474,7 +476,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has("#title", text: "Not main page") end - test "raises an error if one element is found", %{conn: conn} do + also_test_js "raises an error if one element is found", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -493,7 +495,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises an error if multiple elements are found", %{conn: conn} do + also_test_js "raises an error if multiple elements are found", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -516,13 +518,13 @@ defmodule PhoenixTest.AssertionsTest do end end - test "accepts an `exact` option to match text exactly", %{conn: conn} do + also_test_js "accepts an `exact` option to match text exactly", %{conn: conn} do conn |> visit("/page/index") |> refute_has("h1", text: "Main", exact: true) end - test "raises if `exact` text makes refutation false", %{conn: conn} do + also_test_js "raises if `exact` text makes refutation false", %{conn: conn} do msg = ignore_whitespace(""" Expected not to find any elements with selector "h1" and text "Main". @@ -541,18 +543,22 @@ defmodule PhoenixTest.AssertionsTest do end end - test "accepts an `at` option (without text) to refute on a specific element", %{conn: conn} do + also_test_js "accepts an `at` option (without text) to refute on a specific element", %{conn: conn} do conn |> visit("/page/index") |> refute_has("#single-list-item li", at: 2) end - test "accepts an `at` option with text to refute on a specific element", %{conn: conn} do + also_test_js "accepts an `at` option with text to refute on a specific element", %{conn: conn} do conn |> visit("/page/index") |> refute_has("#multiple-items li", at: 2, text: "Aragorn") end + # Different semantics: + # - PhoenixTest: Assert second li has text is "Legolas" + # - Playwright: Assert two li with text "Legolas" exist + @tag playwright: false, reason: :known_inconsistency test "raises if it finds element at `at` position", %{conn: conn} do msg = ignore_whitespace(""" @@ -574,25 +580,25 @@ defmodule PhoenixTest.AssertionsTest do end describe "assert_path" do - test "asserts the session's current path" do + also_test_js "asserts the session's current path" do session = %Live{current_path: "/page/index"} assert_path(session, "/page/index") end - test "asserts query params are the same" do + also_test_js "asserts query params are the same" do session = %Live{current_path: "/page/index?hello=world"} assert_path(session, "/page/index", query_params: %{"hello" => "world"}) end - test "order of query params does not matter" do + also_test_js "order of query params does not matter" do session = %Live{current_path: "/page/index?hello=world&foo=bar"} assert_path(session, "/page/index", query_params: %{"foo" => "bar", "hello" => "world"}) end - test "raises helpful error if path doesn't match" do + also_test_js "raises helpful error if path doesn't match" do msg = ignore_whitespace(""" Expected path to be "/page/not-index" but got "/page/index" @@ -605,7 +611,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises helpful error if path doesn't have query params" do + also_test_js "raises helpful error if path doesn't have query params" do msg = ignore_whitespace(""" Expected query params to be "details=true&foo=bar" but got nil @@ -618,7 +624,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises helpful error if query params don't match" do + also_test_js "raises helpful error if query params don't match" do msg = ignore_whitespace(""" Expected query params to be "goodbye=world&hi=bye" but got "hello=world&hi=bye" @@ -633,19 +639,19 @@ defmodule PhoenixTest.AssertionsTest do end describe "refute_path" do - test "refute the given path is the current path" do + also_test_js "refute the given path is the current path" do session = %Live{current_path: "/page/index"} refute_path(session, "/page/page_2") end - test "refutes query params are the same" do + also_test_js "refutes query params are the same" do session = %Live{current_path: "/page/index?hello=world"} refute_path(session, "/page/index", query_params: %{"hello" => "not-world"}) end - test "raises helpful error if path matches" do + also_test_js "raises helpful error if path matches" do msg = ignore_whitespace(""" Expected path not to be "/page/index" @@ -658,7 +664,7 @@ defmodule PhoenixTest.AssertionsTest do end end - test "raises helpful error if query params MATCH" do + also_test_js "raises helpful error if query params MATCH" do msg = ignore_whitespace(""" Expected query params not to be "hello=world&hi=bye" diff --git a/test/phoenix_test/live_test.exs b/test/phoenix_test/live_test.exs index 38135d03..e8b9e317 100644 --- a/test/phoenix_test/live_test.exs +++ b/test/phoenix_test/live_test.exs @@ -1,64 +1,56 @@ defmodule PhoenixTest.LiveTest do - use ExUnit.Case, async: true + use PhoenixTest.Case, async: true import PhoenixTest - import PhoenixTest.Locators + import PhoenixTest.TestHelpers alias PhoenixTest.Driver - setup do - %{conn: Phoenix.ConnTest.build_conn()} - end - - describe "render_page_title/1" do - test "renders the page title", %{conn: conn} do - title = - conn - |> visit("/live/index") - |> PhoenixTest.Driver.render_page_title() + @moduletag :playwright - assert title == "PhoenixTest is the best!" + describe "assert_has/2 title" do + also_test_js "renders the page title", %{conn: conn} do + conn + |> visit("/live/index") + |> assert_has("title", text: "PhoenixTest is the best!") end - test "renders updated page title", %{conn: conn} do - title = - conn - |> visit("/live/index") - |> click_button("Change page title") - |> PhoenixTest.Driver.render_page_title() - - assert title == "Title changed!" + also_test_js "renders updated page title", %{conn: conn} do + conn + |> visit("/live/index") + |> click_button("Change page title") + |> assert_has("title", text: "Title changed!") end + end - test "returns nil if page title isn't found", %{conn: conn} do - title = - conn - |> visit("/live/index_no_layout") - |> PhoenixTest.Driver.render_page_title() - - assert title == nil + describe "refute_has/1 title" do + also_test_js "returns nil if page title isn't found", %{conn: conn} do + conn + |> visit("/live/index_no_layout") + |> refute_has("title") end end describe "visit/2" do - test "navigates to given LiveView page", %{conn: conn} do + also_test_js "navigates to given LiveView page", %{conn: conn} do conn |> visit("/live/index") |> assert_has("h1", text: "LiveView main page") end - test "follows redirects", %{conn: conn} do + also_test_js "follows redirects", %{conn: conn} do conn |> visit("/live/redirect_on_mount/redirect") |> assert_has("h1", text: "LiveView main page") end - test "follows push redirects (push navigate)", %{conn: conn} do + also_test_js "follows push redirects (push navigate)", %{conn: conn} do conn |> visit("/live/redirect_on_mount/push_navigate") |> assert_has("h1", text: "LiveView main page") end + @tag playwright: false, reason: :irrelevant test "preserves headers across redirects", %{conn: conn} do conn |> Plug.Conn.put_req_header("x-custom-header", "Some-Value") @@ -69,6 +61,7 @@ defmodule PhoenixTest.LiveTest do end) end + @tag playwright: false, reason: :known_inconsistency test "raises error if route doesn't exist", %{conn: conn} do assert_raise ArgumentError, ~r/404/, fn -> visit(conn, "/live/non_route") @@ -77,41 +70,42 @@ defmodule PhoenixTest.LiveTest do end describe "click_link/2" do - test "follows 'navigate' links", %{conn: conn} do + also_test_js "follows 'navigate' links", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate link") |> assert_has("h1", text: "LiveView page 2") end - test "follows navigation that subsequently redirect", %{conn: conn} do + also_test_js "follows navigation that subsequently redirect", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate (and redirect back) link") |> assert_has("h1", text: "LiveView main page") end - test "accepts click_link with selector", %{conn: conn} do + also_test_js "accepts click_link with selector", %{conn: conn} do conn |> visit("/live/index") |> click_link("a", "Navigate link") |> assert_has("h1", text: "LiveView page 2") end - test "handles patches to current view", %{conn: conn} do + also_test_js "handles patches to current view", %{conn: conn} do conn |> visit("/live/index") |> click_link("Patch link") |> assert_has("h2", text: "LiveView main page details") end - test "handles navigation to a non-liveview", %{conn: conn} do + also_test_js "handles navigation to a non-liveview", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate to non-liveview") |> assert_has("h1", text: "Main page") end + @tag playwright: false, reason: :irrelevant test "preserves headers across navigation", %{conn: conn} do conn |> Plug.Conn.put_req_header("x-custom-header", "Some-Value") @@ -123,6 +117,8 @@ defmodule PhoenixTest.LiveTest do end) end + # Playwright: Errors with PhoenixTest message (not Phoenix.LiveViewTest message) + @tag playwright: false, reason: :different_error_message test "raises error when there are multiple links with same text", %{conn: conn} do assert_raise ArgumentError, ~r/2 of them matched the text filter/, fn -> conn @@ -131,7 +127,9 @@ defmodule PhoenixTest.LiveTest do end end - test "raises an error when link element can't be found with given text", %{conn: conn} do + # Playwright: Errors with PhoenixTest message (not Phoenix.LiveViewTest message) + @tag playwright: false, reason: :different_error_message + also_test_js "raises an error when link element can't be found with given text", %{conn: conn} do assert_raise ArgumentError, ~r/elements but none matched the text filter "No link"/, fn -> conn |> visit("/live/index") @@ -139,6 +137,8 @@ defmodule PhoenixTest.LiveTest do end end + # Playwright: Errors with PhoenixTest message (not Phoenix.LiveViewTest message) + @tag playwright: false, reason: :different_error_message test "raises an error when there are no links on the page", %{conn: conn} do assert_raise ArgumentError, ~r/selector "a" did not return any element/, fn -> conn @@ -149,13 +149,14 @@ defmodule PhoenixTest.LiveTest do end describe "click_button/2" do - test "handles a `phx-click` button", %{conn: conn} do + also_test_js "handles a `phx-click` button", %{conn: conn} do conn |> visit("/live/index") |> click_button("Show tab") |> assert_has("#tab", text: "Tab title") end + @tag playwright: false, reason: :irrevelant test "does not remove active form if button isn't form's submit button", %{conn: conn} do session = conn @@ -166,6 +167,7 @@ defmodule PhoenixTest.LiveTest do assert PhoenixTest.ActiveForm.active?(session.active_form) end + @tag playwright: false, reason: :irrevelant test "resets active form if it is form's submit button", %{conn: conn} do session = conn @@ -176,7 +178,7 @@ defmodule PhoenixTest.LiveTest do refute PhoenixTest.ActiveForm.active?(session.active_form) end - test "includes name and value if specified", %{conn: conn} do + also_test_js "includes name and value if specified", %{conn: conn} do conn |> visit("/live/index") |> fill_in("User Name", with: "Aragorn") @@ -184,7 +186,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:no-phx-change-form-button: save") end - test "includes default data if form is untouched", %{conn: conn} do + also_test_js "includes default data if form is untouched", %{conn: conn} do conn |> visit("/live/index") |> click_button("Save Full Form") @@ -196,15 +198,19 @@ defmodule PhoenixTest.LiveTest do |> refute_has("#form-data", text: "disabled_textarea:") end - test "can click button that does not submit form after filling form", %{conn: conn} do + also_test_js "can click button that does not submit form after filling form", %{conn: conn} do conn |> visit("/live/index") - |> fill_in("Email", with: "some@example.com") + |> within("#email-form", fn session -> + fill_in(session, "Email", with: "some@example.com") + end) |> click_button("Save Nested Form") |> refute_has("#form-data", text: "email: some@example.com") end - test "submits owner form if button isn't nested inside form (including button data)", %{conn: conn} do + also_test_js "submits owner form if button isn't nested inside form (including button data)", %{ + conn: conn + } do conn |> visit("/live/index") |> within("#owner-form", fn session -> @@ -215,7 +221,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "form-button: save-owner-form") end - test "follows form's redirect to live page", %{conn: conn} do + also_test_js "follows form's redirect to live page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form", &fill_in(&1, "Name", with: "Aragorn")) @@ -223,7 +229,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("h1", text: "LiveView page 2") end - test "follows form's redirect to static page", %{conn: conn} do + also_test_js "follows form's redirect to static page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form-to-static", &fill_in(&1, "Name", with: "Aragorn")) @@ -231,7 +237,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("h1", text: "Main page") end - test "submits regular (non phx-submit) form", %{conn: conn} do + also_test_js "submits regular (non phx-submit) form", %{conn: conn} do conn |> visit("/live/index") |> within("#non-liveview-form", &fill_in(&1, "Name", with: "Aragorn")) @@ -239,6 +245,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "name: Aragorn") end + @tag playwright: false, reason: :known_inconsistency test "raises an error if form doesn't have a `phx-submit` or `action`", %{conn: conn} do msg = ~r/to have a `phx-submit` or `action` defined/ @@ -250,7 +257,7 @@ defmodule PhoenixTest.LiveTest do end end - test "raises an error when there are no buttons on page", %{conn: conn} do + also_test_js "raises an error when there are no buttons on page", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find an element/, fn -> conn |> visit("/live/page_2") @@ -258,6 +265,7 @@ defmodule PhoenixTest.LiveTest do end end + @tag playwright: false, reason: :known_inconsistency test "raises an error if button is not part of form and has no phx-submit", %{conn: conn} do msg = ~r/to have a valid `phx-click` attribute or belong to a `form` element/ @@ -268,7 +276,7 @@ defmodule PhoenixTest.LiveTest do end end - test "raises an error if active form but can't find button", %{conn: conn} do + also_test_js "raises an error if active form but can't find button", %{conn: conn} do msg = ~r/Could not find an element/ assert_raise ArgumentError, msg, fn -> @@ -284,7 +292,7 @@ defmodule PhoenixTest.LiveTest do end describe "within/3" do - test "scopes assertions within selector", %{conn: conn} do + also_test_js "scopes assertions within selector", %{conn: conn} do conn |> visit("/live/index") |> assert_has("button", text: "Reset") @@ -293,16 +301,16 @@ defmodule PhoenixTest.LiveTest do end) end - test "scopes further form actions within a selector", %{conn: conn} do + also_test_js "scopes further form actions within a selector", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> fill_in(session, "Email", with: "someone@example.com") end) - |> assert_has(input(label: "Email", value: "someone@example.com")) + |> assert_has("#form-data", text: "email: someone@example.com") end - test "raises when data is not in scoped HTML", %{conn: conn} do + also_test_js "raises when data is not in scoped HTML", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with label "User Name"/, fn -> conn |> visit("/live/index") @@ -314,14 +322,16 @@ defmodule PhoenixTest.LiveTest do end describe "fill_in/4" do - test "fills in a single text field based on the label", %{conn: conn} do + also_test_js "fills in a single text field based on the label", %{conn: conn} do conn |> visit("/live/index") - |> fill_in("Email", with: "someone@example.com") - |> assert_has(input(label: "Email", value: "someone@example.com")) + |> within("#email-form", fn session -> + fill_in(session, "Email", with: "someone@example.com") + end) + |> assert_has("#form-data", text: "email: someone@example.com") end - test "can fill input with `nil` to override existing value", %{conn: conn} do + also_test_js "can fill input with `nil` to override existing value", %{conn: conn} do conn |> visit("/live/index") |> within("#pre-rendered-data-form", fn session -> @@ -330,7 +340,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "input's value is empty") end - test "can fill-in textareas", %{conn: conn} do + also_test_js "can fill-in textareas", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Notes", with: "Dunedain. Heir to the throne. King of Arnor and Gondor") @@ -340,7 +350,7 @@ defmodule PhoenixTest.LiveTest do ) end - test "can fill-in complex form fields", %{conn: conn} do + also_test_js "can fill-in complex form fields", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Aragorn") @@ -352,7 +362,7 @@ defmodule PhoenixTest.LiveTest do ) end - test "can fill in numbers", %{conn: conn} do + also_test_js "can fill in numbers", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Level (number)", with: 10) @@ -360,7 +370,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "level: 10") end - test "works in 'nested' forms", %{conn: conn} do + also_test_js "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> fill_in("User Name", with: "Aragorn") @@ -370,24 +380,28 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:role: El Jefe") end - test "can be used to submit form", %{conn: conn} do + also_test_js "can be used to submit form", %{conn: conn} do conn |> visit("/live/index") - |> fill_in("Email", with: "someone@example.com") + |> within("#email-form", fn session -> + fill_in(session, "Email", with: "someone@example.com") + end) |> click_button("Save Email") |> assert_has("#form-data", text: "email: someone@example.com") end - test "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do + also_test_js "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do conn |> visit("/live/index") - |> fill_in("Email", with: "frodo@example.com") + |> within("#email-form", fn session -> + fill_in(session, "Email", with: "frodo@example.com") + end) |> fill_in("Comments", with: "Hobbit") |> assert_has("#form-data", text: "comments: Hobbit") |> refute_has("#form-data", text: "email: frodo@example.com") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -396,7 +410,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "name: Frodo") end - test "can target input with selector if multiple labels have same text", %{conn: conn} do + also_test_js "can target input with selector if multiple labels have same text", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -405,7 +419,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "book-characters: Frodo") end - test "raises an error when element can't be found with label", %{conn: conn} do + also_test_js "raises an error when element can't be found with label", %{conn: conn} do msg = ~r/Could not find element with label "Non-existent Email Label"./ assert_raise ArgumentError, msg, fn -> @@ -415,7 +429,7 @@ defmodule PhoenixTest.LiveTest do end end - test "raises an error when label is found but no corresponding input is found", %{conn: conn} do + also_test_js "raises an error when label is found but no corresponding input is found", %{conn: conn} do msg = ~r/Found label but can't find labeled element whose `id` matches/ assert_raise ArgumentError, msg, fn -> @@ -427,21 +441,21 @@ defmodule PhoenixTest.LiveTest do end describe "select/3" do - test "selects given option for a label", %{conn: conn} do + also_test_js "selects given option for a label", %{conn: conn} do conn |> visit("/live/index") |> select("Elf", from: "Race") |> assert_has("#full-form option[value='elf']") end - test "allows selecting option if a similar option exists", %{conn: conn} do + also_test_js "allows selecting option if a similar option exists", %{conn: conn} do conn |> visit("/live/index") |> select("Orc", from: "Race") |> assert_has("#full-form option[value='orc']") end - test "works in 'nested' forms", %{conn: conn} do + also_test_js "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> select("False", from: "User Admin") @@ -449,7 +463,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:admin: false") end - test "can be used to submit form", %{conn: conn} do + also_test_js "can be used to submit form", %{conn: conn} do conn |> visit("/live/index") |> select("Elf", from: "Race") @@ -457,7 +471,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "race: elf") end - test "works for multiple select", %{conn: conn} do + also_test_js "works for multiple select", %{conn: conn} do conn |> visit("/live/index") |> select("Elf", from: "Race") @@ -466,44 +480,27 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "[elf, dwarf]") end - test "works with phx-click outside of forms", %{conn: conn} do - conn - |> visit("/live/index") - |> within("#not-a-form", fn session -> - select(session, "Dog", from: "Choose a pet:") - end) - |> assert_has("#form-data", text: "selected: [dog]") - end - - test "works with phx-click and multi-select", %{conn: conn} do - conn - |> visit("/live/index") - |> within("#not-a-form", fn session -> - select(session, ["Dog", "Cat"], from: "Choose a pet:") - end) - |> assert_has("#form-data", text: "selected: [dog, cat]") - end - - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> - select(session, "Dog", from: "Choose a pet:", exact: false) + select(session, "Cat", from: "Choose a pet:", exact: false) end) - |> assert_has("#form-data", text: "pet: dog") + |> assert_has("#form-data", text: "pet: cat") end + @tag playwright: false, reason: :not_implemented, not_implemented: :exact_option test "can target an option's text with exact_option: false", %{conn: conn} do conn |> visit("/live/index") |> within("#full-form", fn session -> - select(session, "Hum", from: "Race", exact_option: false) + select(session, "Dwa", from: "Race", exact_option: false) end) |> submit() - |> assert_has("#form-data", text: "race: human") + |> assert_has("#form-data", text: "race: dwarf") end - test "can target option with selector if multiple labels have same text", %{conn: conn} do + also_test_js "can target option with selector if multiple labels have same text", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -512,6 +509,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "favorite-character: Frodo") end + @tag playwright: false, reason: :known_inconsistency test "raises an error if select option is neither in a form nor has a phx-click", %{conn: conn} do session = visit(conn, "/live/index") @@ -522,7 +520,7 @@ defmodule PhoenixTest.LiveTest do end describe "check/3" do - test "checks a checkbox", %{conn: conn} do + also_test_js "checks a checkbox", %{conn: conn} do conn |> visit("/live/index") |> check("Admin") @@ -530,7 +528,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: on") end - test "can check an unchecked checkbox", %{conn: conn} do + also_test_js "can check an unchecked checkbox", %{conn: conn} do conn |> visit("/live/index") |> uncheck("Admin") @@ -539,7 +537,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: on") end - test "handle checkbox name with '?'", %{conn: conn} do + also_test_js "handle checkbox name with '?'", %{conn: conn} do conn |> visit("/live/index") |> check("Subscribe") @@ -547,7 +545,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "subscribe?: on") end - test "works in 'nested' forms", %{conn: conn} do + also_test_js "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> check("Payer") @@ -555,7 +553,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:payer: on") end - test "works with phx-click outside a form", %{conn: conn} do + also_test_js "works with phx-click outside a form", %{conn: conn} do conn |> visit("/live/index") |> within("#not-a-form", fn session -> @@ -564,7 +562,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "value: second-breakfast") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -573,7 +571,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "human: yes") end - test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -582,6 +580,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "like-elixir: yes") end + @tag playwright: false, reason: :known_inconsistency test "raises error if checkbox doesn't have phx-click or belong to form", %{conn: conn} do session = visit(conn, "/live/index") @@ -592,7 +591,7 @@ defmodule PhoenixTest.LiveTest do end describe "uncheck/3" do - test "sends the default value (in hidden input)", %{conn: conn} do + also_test_js "sends the default value (in hidden input)", %{conn: conn} do conn |> visit("/live/index") |> uncheck("Admin") @@ -600,7 +599,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: off") end - test "can uncheck a previous check/2 in the test", %{conn: conn} do + also_test_js "can uncheck a previous check/2 in the test", %{conn: conn} do conn |> visit("/live/index") |> check("Admin") @@ -609,7 +608,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: off") end - test "works in 'nested' forms", %{conn: conn} do + also_test_js "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> check("Payer") @@ -618,7 +617,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:payer: off") end - test "works with phx-click outside a form", %{conn: conn} do + also_test_js "works with phx-click outside a form", %{conn: conn} do conn |> visit("/live/index") |> within("#not-a-form", fn session -> @@ -629,7 +628,7 @@ defmodule PhoenixTest.LiveTest do |> refute_has("#form-data", text: "value: second-breakfast") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -640,7 +639,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "human: no") end - test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -652,6 +651,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "like-elixir: no") end + @tag playwright: false, reason: :known_inconsistency test "raises error if checkbox doesn't have phx-click or belong to form", %{conn: conn} do session = visit(conn, "/live/index") @@ -662,7 +662,7 @@ defmodule PhoenixTest.LiveTest do end describe "choose/3" do - test "chooses an option in radio button", %{conn: conn} do + also_test_js "chooses an option in radio button", %{conn: conn} do conn |> visit("/live/index") |> choose("Email Choice") @@ -670,23 +670,23 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "contact: email") end - test "uses the default 'checked' if present", %{conn: conn} do + also_test_js "uses the default 'checked' if present", %{conn: conn} do conn |> visit("/live/index") |> click_button("Save Full Form") |> assert_has("#form-data", text: "contact: mail") end - test "works with a phx-click outside of a form", %{conn: conn} do + also_test_js "works with a phx-click outside of a form", %{conn: conn} do conn |> visit("/live/index") |> within("#not-a-form", fn session -> - choose(session, "Huey") + choose(session, "Dewey") end) - |> assert_has("#form-data", text: "value: huey") + |> assert_has("#form-data", text: "value: dewey") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -695,7 +695,9 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "book-or-movie: book") end - test "can specify input selector when multiple options have same label in same form", %{conn: conn} do + also_test_js "can specify input selector when multiple options have same label in same form", %{ + conn: conn + } do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -704,6 +706,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "elixir-yes: yes") end + @tag playwright: false, reason: :known_inconsistency test "raises an error if radio is neither in a form nor has a phx-click", %{conn: conn} do session = visit(conn, "/live/index") @@ -714,7 +717,7 @@ defmodule PhoenixTest.LiveTest do end describe "upload/4" do - test "uploads an image", %{conn: conn} do + also_test_js "uploads an image", %{conn: conn} do conn |> visit("/live/index") |> within("#full-form", fn session -> @@ -725,7 +728,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "avatar: elixir.jpg") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -736,7 +739,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "avatar: elixir.jpg") end - test "can specify input selector when multiple inputs have same label", %{conn: conn} do + also_test_js "can specify input selector when multiple inputs have same label", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -747,7 +750,9 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "main_avatar: elixir.jpg") end - test "upload (without other form actions) does not work with submit (matches browser behavior)", %{conn: conn} do + @tag playwright: false, reason: :known_inconsistency + test "upload (without other form actions) does not work with submit (matches browser behavior)", + %{conn: conn} do session = conn |> visit("/live/index") @@ -762,7 +767,7 @@ defmodule PhoenixTest.LiveTest do end describe "filling out full form with field functions" do - test "populates all fields", %{conn: conn} do + also_test_js "populates all fields", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Legolas") @@ -780,7 +785,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "notes: Woodland Elf") end - test "populates all fields in nested forms", %{conn: conn} do + also_test_js "populates all fields in nested forms", %{conn: conn} do conn |> visit("/live/index") |> fill_in("User Name", with: "Legolas") @@ -792,15 +797,17 @@ defmodule PhoenixTest.LiveTest do end describe "submit/1" do - test "submits a pre-filled form via phx-submit", %{conn: conn} do + also_test_js "submits a pre-filled form via phx-submit", %{conn: conn} do conn |> visit("/live/index") - |> fill_in("Email", with: "some@example.com") + |> within("#email-form", fn session -> + fill_in(session, "Email", with: "some@example.com") + end) |> submit() |> assert_has("#form-data", text: "email: some@example.com") end - test "includes pre-rendered data", %{conn: conn} do + also_test_js "includes pre-rendered data", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Aragorn") @@ -810,7 +817,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "contact: mail") end - test "includes the first button's name and value if present", %{conn: conn} do + also_test_js "includes the first button's name and value if present", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Aragorn") @@ -818,7 +825,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "full_form_button: save") end - test "can submit form without button", %{conn: conn} do + also_test_js "can submit form without button", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Country of Origin", with: "Arnor") @@ -826,7 +833,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "country: Arnor") end - test "follows form's redirect to live page", %{conn: conn} do + also_test_js "follows form's redirect to live page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form", fn session -> @@ -837,7 +844,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("h1", text: "LiveView page 2") end - test "follows form's redirect to static page", %{conn: conn} do + also_test_js "follows form's redirect to static page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form-to-static", fn session -> @@ -848,6 +855,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("h1", text: "Main page") end + @tag playwright: false, reason: :irrelevant test "preserves headers after form submission and redirect", %{conn: conn} do conn |> Plug.Conn.put_req_header("x-custom-header", "Some-Value") @@ -863,7 +871,7 @@ defmodule PhoenixTest.LiveTest do end) end - test "submits regular (non phx-submit) form", %{conn: conn} do + also_test_js "submits regular (non phx-submit) form", %{conn: conn} do conn |> visit("/live/index") |> within("#non-liveview-form", fn session -> @@ -876,6 +884,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "button: save") end + @tag playwright: false, reason: :known_inconsistency test "raises an error if there's no active form", %{conn: conn} do message = ~r/There's no active form. Fill in a form with `fill_in`, `select`, etc./ @@ -886,6 +895,7 @@ defmodule PhoenixTest.LiveTest do end end + @tag playwright: false, reason: :known_inconsistency test "raises an error if form doesn't have a `phx-submit` or `action`", %{conn: conn} do msg = ~r/to have a `phx-submit` or `action` defined/ @@ -910,6 +920,7 @@ defmodule PhoenixTest.LiveTest do %{open_fun: open_fun} end + @tag playwright: false, reason: :irrelevant test "opens the browser", %{conn: conn, open_fun: open_fun} do conn |> visit("/live/index") @@ -919,6 +930,7 @@ defmodule PhoenixTest.LiveTest do end describe "unwrap" do + @tag playwright: false, reason: :irrelevant test "provides an escape hatch that gives access to the underlying view", %{conn: conn} do conn |> visit("/live/index") @@ -930,6 +942,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "name: Legolas") end + @tag playwright: false, reason: :irrelevant test "follows redirects after unwrap action", %{conn: conn} do conn |> visit("/live/index") @@ -942,80 +955,78 @@ defmodule PhoenixTest.LiveTest do end end - describe "current_path" do - test "it is set on visit", %{conn: conn} do - session = visit(conn, "/live/index") - - assert PhoenixTest.Driver.current_path(session) == "/live/index" + describe "assert_path" do + also_test_js "it is set on visit", %{conn: conn} do + conn + |> visit("/live/index") + |> assert_path("/live/index") end - test "it is set on visit with query string", %{conn: conn} do - session = visit(conn, "/live/index?foo=bar") - - assert PhoenixTest.Driver.current_path(session) == "/live/index?foo=bar" + also_test_js "it is set on visit with query string", %{conn: conn} do + conn + |> visit("/live/index?foo=bar") + |> assert_path("/live/index", query_params: %{foo: "bar"}) end - test "it is updated on href navigation", %{conn: conn} do - session = - conn - |> visit("/live/index") - |> click_link("Navigate to non-liveview") - - assert PhoenixTest.Driver.current_path(session) == "/page/index?details=true&foo=bar" + also_test_js "it is updated on href navigation", %{conn: conn} do + conn + |> visit("/live/index") + |> click_link("Navigate to non-liveview") + |> assert_path("/page/index", query_params: %{details: "true", foo: "bar"}) end - test "it is updated on live navigation", %{conn: conn} do - session = - conn - |> visit("/live/index") - |> click_link("Navigate link") - - assert PhoenixTest.Driver.current_path(session) == "/live/page_2?details=true&foo=bar" + also_test_js "it is updated on live navigation", %{conn: conn} do + conn + |> visit("/live/index") + |> click_link("Navigate link") + |> assert_path("/live/page_2", query_params: %{details: "true", foo: "bar"}) end - test "it is updated on live patching", %{conn: conn} do - session = - conn - |> visit("/live/index") - |> click_link("Patch link") - - assert PhoenixTest.Driver.current_path(session) == "/live/index?details=true&foo=bar" + also_test_js "it is updated on live patching", %{conn: conn} do + conn + |> visit("/live/index") + |> click_link("Patch link") + |> assert_path("/live/index", query_params: %{details: "true", foo: "bar"}) end - test "it is updated on push navigation", %{conn: conn} do - session = - conn - |> visit("/live/index") - |> click_button("Button with push navigation") - - assert PhoenixTest.Driver.current_path(session) == "/live/page_2?foo=bar" + also_test_js "it is updated on push navigation", %{conn: conn} do + conn + |> visit("/live/index") + |> click_button("Button with push navigation") + |> assert_path("/live/page_2", query_params: %{foo: "bar"}) end - test "it is updated on push patch", %{conn: conn} do - session = - conn - |> visit("/live/index") - |> click_button("Button with push patch") - - assert PhoenixTest.Driver.current_path(session) == "/live/index?foo=bar" + also_test_js "it is updated on push patch", %{conn: conn} do + conn + |> visit("/live/index") + |> click_button("Button with push patch") + |> assert_path("/live/index", query_params: %{foo: "bar"}) end end describe "shared form helpers behavior" do - test "triggers phx-change validations", %{conn: conn} do + also_test_js "triggers phx-change validations", %{conn: conn} do conn |> visit("/live/index") - |> fill_in("Email", with: nil) + |> within("#email-form", fn session -> + session + |> fill_in("Email", with: "email") + |> fill_in("Email", with: nil) + end) |> assert_has("#form-errors", text: "Errors present") end - test "sends _target with phx-change events", %{conn: conn} do + also_test_js "sends _target with phx-change events", %{conn: conn} do conn |> visit("/live/index") - |> fill_in("Email", with: "frodo@example.com") + |> within("#email-form", fn session -> + fill_in(session, "Email", with: "frodo@example.com") + end) |> assert_has("#form-data", text: "_target: [email]") end + # Playwright: Unstable data-phx-ids + @tag playwright: false, reason: :flaky_test test "does not trigger phx-change event if one isn't present", %{conn: conn} do session = visit(conn, "/live/index") @@ -1029,14 +1040,14 @@ defmodule PhoenixTest.LiveTest do assert starting_html == ending_html end - test "follows redirects on phx-change", %{conn: conn} do + also_test_js "follows redirects on phx-change", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Email with redirect", with: "someone@example.com") |> assert_has("h1", text: "LiveView page 2") end - test "preserves correct order of active form vs form data", %{conn: conn} do + also_test_js "preserves correct order of active form vs form data", %{conn: conn} do conn |> visit("/live/index") |> within("#changes-hidden-input-form", fn session -> @@ -1049,6 +1060,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "hidden_race: hobbit") end + @tag playwright: false, reason: :known_inconsistency test "raises an error if field doesn't have a `name` attribute", %{conn: conn} do assert_raise ArgumentError, ~r/Field is missing a `name` attribute/, fn -> conn diff --git a/test/phoenix_test/playwright_test.exs b/test/phoenix_test/playwright_test.exs new file mode 100644 index 00000000..81d205da --- /dev/null +++ b/test/phoenix_test/playwright_test.exs @@ -0,0 +1,26 @@ +defmodule PhoenixTest.PlaywrightTest do + use PhoenixTest.Case, async: true + + import PhoenixTest + + @moduletag :playwright + + describe "render_page_title/1" do + test "uses playwright driver by default", %{conn: conn} do + session = visit(conn, "/live/index") + assert %PhoenixTest.Playwright{} = session + + title = PhoenixTest.Driver.render_page_title(session) + assert title == "PhoenixTest is the best!" + end + + @tag playwright: false + test "'@tag playwright: false' forces live driver", %{conn: conn} do + session = visit(conn, "/live/index") + assert %PhoenixTest.Live{} = session + + title = PhoenixTest.Driver.render_page_title(session) + assert title == "PhoenixTest is the best!" + end + end +end diff --git a/test/phoenix_test/static_test.exs b/test/phoenix_test/static_test.exs index 6b741e12..ff567926 100644 --- a/test/phoenix_test/static_test.exs +++ b/test/phoenix_test/static_test.exs @@ -1,15 +1,13 @@ defmodule PhoenixTest.StaticTest do - use ExUnit.Case, async: true + use PhoenixTest.Case, async: true import PhoenixTest import PhoenixTest.TestHelpers - setup do - %{conn: Phoenix.ConnTest.build_conn()} - end + @moduletag :playwright describe "render_page_title/1" do - test "renders the page title", %{conn: conn} do + also_test_js "renders the page title", %{conn: conn} do title = conn |> visit("/page/index") @@ -18,7 +16,7 @@ defmodule PhoenixTest.StaticTest do assert title == "PhoenixTest is the best!" end - test "renders nil if there's no page title", %{conn: conn} do + also_test_js "renders nil if there's no page title", %{conn: conn} do title = conn |> visit("/page/index_no_layout") @@ -29,18 +27,19 @@ defmodule PhoenixTest.StaticTest do end describe "visit/2" do - test "navigates to given static page", %{conn: conn} do + also_test_js "navigates to given static page", %{conn: conn} do conn |> visit("/page/index") |> assert_has("h1", text: "Main page") end - test "follows redirects", %{conn: conn} do + also_test_js "follows redirects", %{conn: conn} do conn |> visit("/page/redirect_to_static") |> assert_has("h1", text: "Main page") end + @tag playwright: false, reason: :irrelevant test "preserves headers across redirects", %{conn: conn} do conn |> Plug.Conn.put_req_header("x-custom-header", "Some-Value") @@ -51,6 +50,7 @@ defmodule PhoenixTest.StaticTest do end) end + @tag playwright: false, reason: :known_inconsistency test "raises error if route doesn't exist", %{conn: conn} do assert_raise ArgumentError, ~r/404/, fn -> visit(conn, "/non_route") @@ -59,27 +59,28 @@ defmodule PhoenixTest.StaticTest do end describe "click_link/2" do - test "follows link's path", %{conn: conn} do + also_test_js "follows link's path", %{conn: conn} do conn |> visit("/page/index") |> click_link("Page 2") |> assert_has("h1", text: "Page 2") end - test "follows link that subsequently redirects", %{conn: conn} do + also_test_js "follows link that subsequently redirects", %{conn: conn} do conn |> visit("/page/index") |> click_link("Navigate away and redirect back") |> assert_has("h1", text: "Main page") end - test "accepts selector for link", %{conn: conn} do + also_test_js "accepts selector for link", %{conn: conn} do conn |> visit("/page/index") |> click_link("a", "Page 2") |> assert_has("h1", text: "Page 2") end + @tag playwright: false, reason: :irrelevant test "preserves headers across navigation", %{conn: conn} do conn |> Plug.Conn.put_req_header("x-custom-header", "Some-Value") @@ -91,13 +92,15 @@ defmodule PhoenixTest.StaticTest do end) end - test "handles navigation to a LiveView", %{conn: conn} do + also_test_js "handles navigation to a LiveView", %{conn: conn} do conn |> visit("/page/index") |> click_link("To LiveView!") |> assert_has("h1", text: "LiveView main page") end + # Playwright: case insensitive when using exact=false semantics to find link by substring. + @tag playwright: false, reason: :known_inconsistency test "handles form submission via `data-method` & `data-to` attributes", %{conn: conn} do conn |> visit("/page/index") @@ -105,6 +108,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("h1", text: "Record deleted") end + @tag playwright: false, reason: :known_inconsistency test "raises error if trying to submit via `data-` attributes but incomplete", %{conn: conn} do msg = ignore_whitespace(""" @@ -135,7 +139,7 @@ defmodule PhoenixTest.StaticTest do end end - test "raises error when there are multiple links with same text", %{conn: conn} do + also_test_js "raises error when there are multiple links with same text", %{conn: conn} do assert_raise ArgumentError, ~r/Found more than one element with selector/, fn -> conn |> visit("/page/index") @@ -143,7 +147,7 @@ defmodule PhoenixTest.StaticTest do end end - test "raises an error when link element can't be found with given text", %{conn: conn} do + also_test_js "raises an error when link element can't be found with given text", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with selector/, fn -> conn |> visit("/page/index") @@ -151,7 +155,7 @@ defmodule PhoenixTest.StaticTest do end end - test "raises an error when there are no links on the page", %{conn: conn} do + also_test_js "raises an error when there are no links on the page", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with selector/, fn -> conn |> visit("/page/page_2") @@ -161,35 +165,35 @@ defmodule PhoenixTest.StaticTest do end describe "click_button/2" do - test "handles a button that defaults to GET", %{conn: conn} do + also_test_js "handles a button that defaults to GET", %{conn: conn} do conn |> visit("/page/index") |> click_button("Get record") |> assert_has("h1", text: "Record received") end - test "accepts selector for button", %{conn: conn} do + also_test_js "accepts selector for button", %{conn: conn} do conn |> visit("/page/index") |> click_button("button", "Get record") |> assert_has("h1", text: "Record received") end - test "handles a button clicks when button PUTs data (hidden input)", %{conn: conn} do + also_test_js "handles a button clicks when button PUTs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Mark as active") |> assert_has("h1", text: "Record updated") end - test "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do + also_test_js "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Delete record") |> assert_has("h1", text: "Record deleted") end - test "can submit forms with input type submit", %{conn: conn} do + also_test_js "can submit forms with input type submit", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "sample@example.com") @@ -197,7 +201,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "email: sample@example.com") end - test "can handle clicking button that does not submit form after filling a form", %{conn: conn} do + also_test_js "can handle clicking button that does not submit form after filling a form", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "some@example.com") @@ -205,7 +209,7 @@ defmodule PhoenixTest.StaticTest do |> refute_has("#form-data", text: "email: some@example.com") end - test "submits owner form if button isn't nested inside form", %{conn: conn} do + also_test_js "submits owner form if button isn't nested inside form", %{conn: conn} do conn |> visit("/page/index") |> within("#owner-form", fn session -> @@ -215,20 +219,21 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Aragorn") end - test "can handle redirects to a LiveView", %{conn: conn} do + also_test_js "can handle redirects to a LiveView", %{conn: conn} do conn |> visit("/page/index") |> click_button("Post and Redirect") |> assert_has("h1", text: "LiveView main page") end - test "handles form submission via `data-method` & `data-to` attributes", %{conn: conn} do + also_test_js "handles form submission via `data-method` & `data-to` attributes", %{conn: conn} do conn |> visit("/page/index") |> click_button("Data-method Delete") |> assert_has("h1", text: "Record deleted") end + @tag playwright: false, reason: :irrelevant test "does not remove active form if button isn't form's submit button", %{conn: conn} do session = conn @@ -239,6 +244,7 @@ defmodule PhoenixTest.StaticTest do assert PhoenixTest.ActiveForm.active?(session.active_form) end + @tag playwright: false, reason: :irrelevant test "resets active form if it is form's submit button", %{conn: conn} do session = conn @@ -249,7 +255,7 @@ defmodule PhoenixTest.StaticTest do refute PhoenixTest.ActiveForm.active?(session.active_form) end - test "includes name and value if specified", %{conn: conn} do + also_test_js "includes name and value if specified", %{conn: conn} do conn |> visit("/page/index") |> fill_in("User Name", with: "Aragorn") @@ -257,7 +263,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:save-button: nested-form-save") end - test "can handle clicking button that does not submit form after fill_in", %{conn: conn} do + also_test_js "can handle clicking button that does not submit form after fill_in", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "some@example.com") @@ -265,7 +271,7 @@ defmodule PhoenixTest.StaticTest do |> refute_has("#form-data", text: "email: some@example.com") end - test "includes default data if form is untouched", %{conn: conn} do + also_test_js "includes default data if form is untouched", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") @@ -277,6 +283,7 @@ defmodule PhoenixTest.StaticTest do |> refute_has("#form-data", text: "disabled_textarea:") end + @tag playwright: false, reason: :known_inconsistency test "raises error if trying to submit via `data-` attributes but incomplete", %{conn: conn} do msg = ~r/Tried submitting form via `data-method` but some data attributes/ @@ -287,7 +294,7 @@ defmodule PhoenixTest.StaticTest do end end - test "raises an error when there are no buttons on page", %{conn: conn} do + also_test_js "raises an error when there are no buttons on page", %{conn: conn} do msg = ~r/Could not find an element with given selectors/ assert_raise ArgumentError, msg, fn -> @@ -297,7 +304,7 @@ defmodule PhoenixTest.StaticTest do end end - test "raises an error if can't find button", %{conn: conn} do + also_test_js "raises an error if can't find button", %{conn: conn} do msg = ~r/Could not find an element with given selectors/ assert_raise ArgumentError, msg, fn -> @@ -307,6 +314,7 @@ defmodule PhoenixTest.StaticTest do end end + @tag playwright: false, reason: :known_inconsistency test "raises an error if button is not part of form", %{conn: conn} do msg = ~r/Could not find "form" for an element with selector/ @@ -320,7 +328,7 @@ defmodule PhoenixTest.StaticTest do end describe "within/3" do - test "scopes assertions within selector", %{conn: conn} do + also_test_js "scopes assertions within selector", %{conn: conn} do conn |> visit("/page/index") |> assert_has("button", text: "Get record") @@ -329,7 +337,7 @@ defmodule PhoenixTest.StaticTest do end) end - test "scopes further form actions within a selector", %{conn: conn} do + also_test_js "scopes further form actions within a selector", %{conn: conn} do conn |> visit("/page/index") |> within("#email-form", fn session -> @@ -340,7 +348,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "email: someone@example.com") end - test "raises when data is not in scoped HTML", %{conn: conn} do + also_test_js "raises when data is not in scoped HTML", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with label "User Name"/, fn -> conn |> visit("/page/index") @@ -352,7 +360,7 @@ defmodule PhoenixTest.StaticTest do end describe "fill_in/4" do - test "fills in a single text field based on the label", %{conn: conn} do + also_test_js "fills in a single text field based on the label", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "someone@example.com") @@ -360,7 +368,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "email: someone@example.com") end - test "can fill input with `nil` to override existing value", %{conn: conn} do + also_test_js "can fill input with `nil` to override existing value", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Pre Rendered Input", with: nil) @@ -368,7 +376,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "input's value is empty") end - test "can fill-in complex form fields", %{conn: conn} do + also_test_js "can fill-in complex form fields", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -380,7 +388,7 @@ defmodule PhoenixTest.StaticTest do ) end - test "can fill in numbers", %{conn: conn} do + also_test_js "can fill in numbers", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Level (number)", with: 10) @@ -388,7 +396,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "level: 10") end - test "works in 'nested' forms", %{conn: conn} do + also_test_js "works in 'nested' forms", %{conn: conn} do conn |> visit("/page/index") |> fill_in("User Name", with: "Aragorn") @@ -399,7 +407,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:role: El Jefe") end - test "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do + also_test_js "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -409,7 +417,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:name: Legolas") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -419,7 +427,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Frodo") end - test "can target input with selector if multiple labels have same text", %{conn: conn} do + also_test_js "can target input with selector if multiple labels have same text", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -429,7 +437,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "book-characters: Frodo") end - test "raises an error when element can't be found with label", %{conn: conn} do + also_test_js "raises an error when element can't be found with label", %{conn: conn} do msg = ~r/Could not find element with label "Non-existent Email Label"./ assert_raise ArgumentError, msg, fn -> @@ -439,7 +447,7 @@ defmodule PhoenixTest.StaticTest do end end - test "raises an error when label is found but no corresponding input is found", %{conn: conn} do + also_test_js "raises an error when label is found but no corresponding input is found", %{conn: conn} do msg = ~r/Found label but can't find labeled element whose `id` matches/ assert_raise ArgumentError, msg, fn -> @@ -451,7 +459,7 @@ defmodule PhoenixTest.StaticTest do end describe "select/3" do - test "selects given option for a label", %{conn: conn} do + also_test_js "selects given option for a label", %{conn: conn} do conn |> visit("/page/index") |> select("Elf", from: "Race") @@ -459,21 +467,21 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race: elf") end - test "picks first by default", %{conn: conn} do + also_test_js "picks first by default", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") |> assert_has("#form-data", text: "race: human") end - test "allows selecting option if a similar option exists", %{conn: conn} do + also_test_js "allows selecting option if a similar option exists", %{conn: conn} do conn |> visit("/page/index") |> select("Orc", from: "Race") |> assert_has("#full-form option[value='orc']") end - test "works in 'nested' forms", %{conn: conn} do + also_test_js "works in 'nested' forms", %{conn: conn} do conn |> visit("/page/index") |> select("False", from: "User Admin") @@ -481,7 +489,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:admin: false") end - test "handles multi select", %{conn: conn} do + also_test_js "handles multi select", %{conn: conn} do conn |> visit("/page/index") |> select(["Elf", "Dwarf"], from: "Race 2") @@ -489,13 +497,14 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race_2: [elf,dwarf]") end - test "contains no data for empty multi select", %{conn: conn} do + also_test_js "contains no data for empty multi select", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") |> refute_has("#form-data", text: "race_2") end + @tag playwright: false, reason: :flaky_test test "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") @@ -506,6 +515,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "pet: dog") end + @tag playwright: false, reason: :not_implemented, not_implemented: :exact_option test "can target an option's text with exact_option: false", %{conn: conn} do conn |> visit("/page/index") @@ -516,6 +526,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race: human") end + @tag playwright: false, reason: :flaky_test test "can target option with selector if multiple labels have same text", %{conn: conn} do conn |> visit("/page/index") @@ -528,7 +539,7 @@ defmodule PhoenixTest.StaticTest do end describe "check/3" do - test "checks a checkbox", %{conn: conn} do + also_test_js "checks a checkbox", %{conn: conn} do conn |> visit("/page/index") |> check("Admin (boolean)") @@ -536,7 +547,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin_boolean: true") end - test "sets checkbox value as 'on' by default", %{conn: conn} do + also_test_js "sets checkbox value as 'on' by default", %{conn: conn} do conn |> visit("/page/index") |> check("Admin") @@ -544,7 +555,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: on") end - test "can check an unchecked checkbox", %{conn: conn} do + also_test_js "can check an unchecked checkbox", %{conn: conn} do conn |> visit("/page/index") |> uncheck("Admin") @@ -553,7 +564,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: on") end - test "handle checkbox name with '?'", %{conn: conn} do + also_test_js "handle checkbox name with '?'", %{conn: conn} do conn |> visit("/page/index") |> check("Subscribe") @@ -561,7 +572,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "subscribe?: on") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -571,7 +582,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "human: yes") end - test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -583,7 +594,7 @@ defmodule PhoenixTest.StaticTest do end describe "uncheck/3" do - test "sends the default value (in hidden input)", %{conn: conn} do + also_test_js "sends the default value (in hidden input)", %{conn: conn} do conn |> visit("/page/index") |> uncheck("Admin") @@ -591,7 +602,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: off") end - test "can uncheck a previous check/2 in the test", %{conn: conn} do + also_test_js "can uncheck a previous check/2 in the test", %{conn: conn} do conn |> visit("/page/index") |> check("Admin") @@ -600,7 +611,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: off") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -612,7 +623,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "human: no") end - test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -627,7 +638,7 @@ defmodule PhoenixTest.StaticTest do end describe "choose/3" do - test "chooses an option in radio button", %{conn: conn} do + also_test_js "chooses an option in radio button", %{conn: conn} do conn |> visit("/page/index") |> choose("Email Choice") @@ -635,14 +646,14 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "contact: email") end - test "uses the default 'checked' if present", %{conn: conn} do + also_test_js "uses the default 'checked' if present", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") |> assert_has("#form-data", text: "contact: mail") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -652,7 +663,9 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "book-or-movie: book") end - test "can specify input selector when multiple options have same label in same form", %{conn: conn} do + also_test_js "can specify input selector when multiple options have same label in same form", %{ + conn: conn + } do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -665,7 +678,7 @@ defmodule PhoenixTest.StaticTest do end describe "upload/4" do - test "uploads image", %{conn: conn} do + also_test_js "uploads image", %{conn: conn} do conn |> visit("/page/index") |> within("#file-upload-form", fn session -> @@ -676,6 +689,8 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "avatar: elixir.jpg") end + # Playwright: Can't find second input field (order of `upload` calls seems to matter, why?) + @tag playwright: false, reason: :bug test "uploads image list", %{conn: conn} do conn |> visit("/page/index") @@ -686,7 +701,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "avatars:[]: phoenix.jpg") end - test "uploads an image in nested forms", %{conn: conn} do + also_test_js "uploads an image in nested forms", %{conn: conn} do conn |> visit("/page/index") |> upload("Nested Avatar", "test/files/elixir.jpg") @@ -694,7 +709,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:avatar: elixir.jpg") end - test "can target a label with exact: false", %{conn: conn} do + also_test_js "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -705,7 +720,9 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "avatar: elixir.jpg") end - test "can specify input selector when multiple inputs have same label", %{conn: conn} do + # Playwright: 'Enter' key on file input oppens file picker dialog + @tag playwright: false, reason: :bug + test("can specify input selector when multiple inputs have same label", %{conn: conn}) do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -717,7 +734,7 @@ defmodule PhoenixTest.StaticTest do end describe "filling out full form with field functions" do - test "populates all fields", %{conn: conn} do + also_test_js "populates all fields", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Legolas") @@ -735,7 +752,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "notes: Woodland Elf") end - test "populates all fields in nested forms", %{conn: conn} do + also_test_js "populates all fields in nested forms", %{conn: conn} do conn |> visit("/page/index") |> fill_in("User Name", with: "Legolas") @@ -747,7 +764,7 @@ defmodule PhoenixTest.StaticTest do end describe "submit/1" do - test "submits form even if no submit is present (acts as )", %{conn: conn} do + also_test_js "submits form even if no submit is present (acts as )", %{conn: conn} do conn |> visit("/page/index") |> within("#no-submit-button-form", fn session -> @@ -758,8 +775,8 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Aragorn") end - test "includes pre-rendered data (input value, selected option, checked checkbox, checked radio button)", - %{conn: conn} do + also_test_js "includes pre-rendered data (input value, selected option, checked checkbox, checked radio button)", + %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -768,7 +785,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race: human") end - test "includes the first button's name and value if present", %{conn: conn} do + also_test_js "includes the first button's name and value if present", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -776,7 +793,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "full_form_button: save") end - test "can submit form without button", %{conn: conn} do + also_test_js "can submit form without button", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Country of Origin", with: "Arnor") @@ -784,7 +801,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "country: Arnor") end - test "updates current_path on submit", %{conn: conn} do + also_test_js "updates current_path on submit", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -792,7 +809,7 @@ defmodule PhoenixTest.StaticTest do |> assert_path("/page/create_record") end - test "can handle redirects", %{conn: conn} do + also_test_js "can handle redirects", %{conn: conn} do conn |> visit("/page/index") |> within("#no-submit-button-and-redirect", fn session -> @@ -803,6 +820,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("h1", text: "LiveView main page") end + @tag playwright: false, reason: :irrelevant test "preserves headers after form submission and redirect", %{conn: conn} do conn |> Plug.Conn.put_req_header("x-custom-header", "Some-Value") @@ -818,7 +836,7 @@ defmodule PhoenixTest.StaticTest do end) end - test "handles when form PUTs data through hidden input", %{conn: conn} do + also_test_js "handles when form PUTs data through hidden input", %{conn: conn} do conn |> visit("/page/index") |> within("#update-form", fn session -> @@ -829,13 +847,14 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Aragorn") end - test "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do + also_test_js "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Delete record") |> assert_has("h1", text: "Record deleted") end + @tag playwright: false, reason: :known_inconsistency test "raises an error if there's no active form", %{conn: conn} do msg = ~r/There's no active form. Fill in a form with `fill_in`, `select`, etc./ @@ -870,7 +889,7 @@ defmodule PhoenixTest.StaticTest do %{open_fun: open_fun} end - test "opens the browser ", %{conn: conn, open_fun: open_fun} do + also_test_js "opens the browser ", %{conn: conn, open_fun: open_fun} do conn |> visit("/page/index") |> open_browser(open_fun) @@ -883,6 +902,7 @@ defmodule PhoenixTest.StaticTest do @endpoint Application.compile_env(:phoenix_test, :endpoint) + @tag playwright: false, reason: :irrelevant test "provides an escape hatch that gives access to the underlying conn", %{conn: conn} do conn |> visit("/page/index") @@ -894,6 +914,7 @@ defmodule PhoenixTest.StaticTest do end) end + @tag playwright: false, reason: :irrelevant test "follows redirects after unwrap action", %{conn: conn} do conn |> visit("/page/page_2") @@ -904,39 +925,36 @@ defmodule PhoenixTest.StaticTest do end end - describe "current_path" do - test "it is set on visit", %{conn: conn} do - session = visit(conn, "/page/index") - - assert PhoenixTest.Driver.current_path(session) == "/page/index" + describe "assert_path" do + also_test_js "it is set on visit", %{conn: conn} do + conn + |> visit("/page/index") + |> assert_path("/page/index") end - test "it includes query string if available", %{conn: conn} do - session = visit(conn, "/page/index?foo=bar") - - assert PhoenixTest.Driver.current_path(session) == "/page/index?foo=bar" + also_test_js "it includes query string if available", %{conn: conn} do + conn + |> visit("/page/index?foo=bar") + |> assert_path("/page/index", query_params: %{foo: "bar"}) end - test "it is updated on href navigation", %{conn: conn} do - session = - conn - |> visit("/page/index") - |> click_link("Page 2") - - assert PhoenixTest.Driver.current_path(session) == "/page/page_2?foo=bar" + also_test_js "it is updated on href navigation", %{conn: conn} do + conn + |> visit("/page/index") + |> click_link("Page 2") + |> assert_path("/page/page_2", query_params: %{foo: "bar"}) end - test "it is updated on redirects", %{conn: conn} do - session = - conn - |> visit("/page/index") - |> click_link("Navigate away and redirect back") - - assert PhoenixTest.Driver.current_path(session) == "/page/index" + also_test_js "it is updated on redirects", %{conn: conn} do + conn + |> visit("/page/index") + |> click_link("Navigate away and redirect back") + |> assert_path("/page/index") end end describe "shared form helpers behavior" do + @tag playwright: false, reason: :known_inconsistency test "raises an error if field doesn't have a `name` attribute", %{conn: conn} do assert_raise ArgumentError, ~r/Field is missing a `name` attribute/, fn -> conn diff --git a/test/support/index_live.ex b/test/support/index_live.ex index c6bcbda0..5e24b6cb 100644 --- a/test/support/index_live.ex +++ b/test/support/index_live.ex @@ -39,7 +39,7 @@ defmodule PhoenixTest.IndexLive do -
+ @@ -127,26 +127,26 @@ defmodule PhoenixTest.IndexLive do
-
+ - + - + - + - + - @@ -162,7 +162,7 @@ defmodule PhoenixTest.IndexLive do -
+
Please select your preferred contact method:
@@ -175,7 +175,7 @@ defmodule PhoenixTest.IndexLive do
- @@ -258,7 +258,7 @@ defmodule PhoenixTest.IndexLive do -
+ @@ -268,7 +268,7 @@ defmodule PhoenixTest.IndexLive do Human * - + - + Do you like Erlang @@ -406,12 +406,6 @@ defmodule PhoenixTest.IndexLive do
- - -
Select to get second breakfast: @@ -422,6 +416,7 @@ defmodule PhoenixTest.IndexLive do id="second-breakfast" name="second-breakfast" value="second-breakfast" + phx-update="ignore" /> @@ -466,6 +461,7 @@ defmodule PhoenixTest.IndexLive do |> allow_upload(:avatar, accept: ~w(.jpg .jpeg)) |> allow_upload(:main_avatar, accept: ~w(.jpg .jpeg)) |> allow_upload(:backup_avatar, accept: ~w(.jpg .jpeg)) + |> allow_upload(:complex_avatar, accept: ~w(.jpg .jpeg)) } end @@ -477,6 +473,15 @@ defmodule PhoenixTest.IndexLive do {:noreply, assign(socket, :show_tab, true)} end + def handle_event("validate-form", form_data, socket) do + { + :noreply, + socket + |> assign(:form_saved, true) + |> assign(:form_data, form_data) + } + end + def handle_event("save-form", form_data, socket) do avatars = consume_uploaded_entries(socket, :avatar, fn _, %{client_name: name} -> @@ -486,10 +491,14 @@ defmodule PhoenixTest.IndexLive do main_avatars = consume_uploaded_entries(socket, :main_avatar, fn _, %{client_name: name} -> {:ok, name} end) + complex_avatars = + consume_uploaded_entries(socket, :complex_avatar, fn _, %{client_name: name} -> {:ok, name} end) + form_data = form_data |> Map.put("avatar", List.first(avatars)) |> Map.put("main_avatar", List.first(main_avatars)) + |> Map.put("complex_avatar", List.first(complex_avatars)) { :noreply, @@ -562,19 +571,6 @@ defmodule PhoenixTest.IndexLive do |> then(&{:noreply, &1}) end - def handle_event("select-pet", %{"value" => value}, socket) do - form_data = - case socket.assigns.form_data do - %{selected: values} -> %{selected: values ++ [value]} - %{} -> %{selected: [value]} - end - - socket - |> assign(:form_saved, true) - |> assign(:form_data, form_data) - |> then(&{:noreply, &1}) - end - def handle_event("toggle-second-breakfast", params, socket) do socket |> assign(:form_saved, true) diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index a5906dc7..202139a7 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -1,6 +1,8 @@ defmodule PhoenixTest.TestHelpers do @moduledoc false + require ExUnit.Case + @doc """ Converts a multi-line string into a whitespace-forgiving regex """ @@ -12,4 +14,15 @@ defmodule PhoenixTest.TestHelpers do |> Enum.map_join("\n", fn s -> "\\s*" <> s <> "\\s*" end) |> Regex.compile!([:dotall]) end + + defmacro also_test_js(message, var \\ quote(do: _), contents) do + quote location: :keep do + tags = Module.get_attribute(__MODULE__, :tag) + @tag playwright: false + ExUnit.Case.test(unquote(message), unquote(var), unquote(contents)) + + for tag <- tags, do: @tag(tag) + ExUnit.Case.test(unquote(message) <> " (Playwright)", unquote(var), unquote(contents)) + end + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 3dd79ff0..5703ed83 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,3 +2,5 @@ ExUnit.start() {:ok, _} = Supervisor.start_link([{Phoenix.PubSub, name: PhoenixTest.PubSub}], strategy: :one_for_one) {:ok, _} = PhoenixTest.Endpoint.start_link() + +Application.put_env(:phoenix_test, :base_url, PhoenixTest.Endpoint.url()) From fbbf51a164c5f40b2a209673e120adb8bde78e06 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Wed, 30 Oct 2024 12:03:44 +0100 Subject: [PATCH 2/6] Use ExUnit 1.18 parameterized tests instead of also_test_js macro --- .github/workflows/ci.yml | 2 +- .tool-versions | 2 +- lib/phoenix_test/case.ex | 19 +-- test/phoenix_test/assertions_test.exs | 124 +++++++++--------- test/phoenix_test/live_test.exs | 175 +++++++++++++------------- test/phoenix_test/playwright_test.exs | 18 +-- test/phoenix_test/static_test.exs | 158 ++++++++++++----------- test/support/test_helpers.ex | 13 -- 8 files changed, 248 insertions(+), 263 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eff386c..3b97548d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: otp: ['26.1.2'] - elixir: ['1.15.0', '1.16.0', '1.17.0'] + elixir: ['1.15.0', '1.16.0', '1.17.0', 'main'] steps: - name: Set up Elixir uses: erlef/setup-beam@v1 diff --git a/.tool-versions b/.tool-versions index d6b59dd2..a1073d9c 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ erlang 27.0 -elixir 1.17.2-otp-27 +elixir main diff --git a/lib/phoenix_test/case.ex b/lib/phoenix_test/case.ex index f9ddc1ee..0d711277 100644 --- a/lib/phoenix_test/case.ex +++ b/lib/phoenix_test/case.ex @@ -5,18 +5,20 @@ defmodule PhoenixTest.Case do alias PhoenixTest.Case - @playwright_opts %{ + @playwright_opts [ browser: :chromium, headless: true, slowMo: 0 - } + ] setup_all context do case context do - %{playwright: opts} -> - opts = Map.merge(@playwright_opts, if(opts == true, do: %{}, else: Map.new(opts))) - browser_id = Case.Playwright.launch_browser(opts) - [playwright: true, browser_id: browser_id] + %{playwright: true} -> + [browser_id: Case.Playwright.launch_browser(@playwright_opts)] + + %{playwright: opts} when is_list(opts) -> + opts = Keyword.merge(@playwright_opts, opts) + [browser_id: Case.Playwright.launch_browser(opts)] _ -> :ok @@ -25,8 +27,8 @@ defmodule PhoenixTest.Case do setup context do case context do - %{playwright: false} -> [conn: Phoenix.ConnTest.build_conn()] - %{browser_id: browser_id} -> [conn: Case.Playwright.session(browser_id)] + %{playwright: p, browser_id: browser_id} when p != false -> [conn: Case.Playwright.session(browser_id)] + _ -> [conn: Phoenix.ConnTest.build_conn()] end end @@ -35,6 +37,7 @@ defmodule PhoenixTest.Case do import PhoenixTest.Playwright.Connection def launch_browser(opts) do + opts = Map.new(opts) ensure_started(opts) browser_id = launch_browser(opts.browser, opts) on_exit(fn -> sync_post(guid: browser_id, method: "close") end) diff --git a/test/phoenix_test/assertions_test.exs b/test/phoenix_test/assertions_test.exs index 5ebdcae2..c4d4ea67 100644 --- a/test/phoenix_test/assertions_test.exs +++ b/test/phoenix_test/assertions_test.exs @@ -1,5 +1,5 @@ defmodule PhoenixTest.AssertionsTest do - use PhoenixTest.Case, async: true + use PhoenixTest.Case, async: true, parameterize: [%{playwright: false}, %{playwright: true}] import PhoenixTest import PhoenixTest.Locators @@ -8,16 +8,14 @@ defmodule PhoenixTest.AssertionsTest do alias ExUnit.AssertionError alias PhoenixTest.Live - @moduletag :playwright - describe "assert_has/2" do - also_test_js "succeeds if single element is found with CSS selector", %{conn: conn} do + test "succeeds if single element is found with CSS selector", %{conn: conn} do conn |> visit("/page/index") |> assert_has("[data-role='title']") end - also_test_js "raises an error if the element cannot be found at all", %{conn: conn} do + test "raises an error if the element cannot be found at all", %{conn: conn} do conn = visit(conn, "/page/index") msg = ~r/Could not find any elements with selector "#nonexistent-id"/ @@ -27,25 +25,25 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "succeeds if element searched is title (Static)", %{conn: conn} do + test "succeeds if element searched is title (Static)", %{conn: conn} do conn |> visit("/page/index") |> assert_has("title") end - also_test_js "succeeds if element searched is title (Live)", %{conn: conn} do + test "succeeds if element searched is title (Live)", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title") end - also_test_js "succeeds if more than one element matches selector", %{conn: conn} do + test "succeeds if more than one element matches selector", %{conn: conn} do conn |> visit("/page/index") |> assert_has("li") end - also_test_js "takes in input helper in assertion", %{conn: conn} do + test "takes in input helper in assertion", %{conn: conn} do conn |> visit("/page/index") |> assert_has(input(type: "text", label: "User Name")) @@ -53,7 +51,7 @@ defmodule PhoenixTest.AssertionsTest do end describe "assert_has/3" do - also_test_js "succeeds if single element is found with CSS selector and text (Static)", %{conn: conn} do + test "succeeds if single element is found with CSS selector and text (Static)", %{conn: conn} do conn |> visit("/page/index") |> assert_has("h1", text: "Main page") @@ -62,7 +60,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("[data-role='title']", text: "Main page") end - also_test_js "succeeds if single element is found with CSS selector and text (Live)", %{conn: conn} do + test "succeeds if single element is found with CSS selector and text (Live)", %{conn: conn} do conn |> visit("/live/index") |> assert_has("h1", text: "LiveView main page") @@ -71,7 +69,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("[data-role='title']", text: "LiveView main page") end - also_test_js "succeeds if more than one element matches selector but text narrows it down", %{ + test "succeeds if more than one element matches selector but text narrows it down", %{ conn: conn } do conn @@ -79,25 +77,25 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("li", text: "Aragorn") end - also_test_js "succeeds if more than one element matches selector and text", %{conn: conn} do + test "succeeds if more than one element matches selector and text", %{conn: conn} do conn |> visit("/page/index") |> assert_has(".multiple_links", text: "Multiple links") end - also_test_js "succeeds if text difference is only a matter of truncation", %{conn: conn} do + test "succeeds if text difference is only a matter of truncation", %{conn: conn} do conn |> visit("/page/index") |> assert_has(".has_extra_space", text: "Has extra space") end - also_test_js "succeeds when a non-200 status code is returned", %{conn: conn} do + test "succeeds when a non-200 status code is returned", %{conn: conn} do conn |> visit("/page/unauthorized") |> assert_has("h1", text: "Unauthorized") end - also_test_js "raises an error if the element cannot be found at all", %{conn: conn} do + test "raises an error if the element cannot be found at all", %{conn: conn} do conn = visit(conn, "/page/index") msg = ~r/Could not find any elements with selector "#nonexistent-id"/ @@ -107,7 +105,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises error if element cannot be found but selector matches other elements", %{ + test "raises error if element cannot be found but selector matches other elements", %{ conn: conn } do conn = visit(conn, "/page/index") @@ -128,25 +126,25 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "can be used to assert on page title (Static)", %{conn: conn} do + test "can be used to assert on page title (Static)", %{conn: conn} do conn |> visit("/page/index") |> assert_has("title", text: "PhoenixTest is the best!") end - also_test_js "can be used to assert on page title (Live)", %{conn: conn} do + test "can be used to assert on page title (Live)", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title", text: "PhoenixTest is the best!") end - also_test_js "can assert title's exactness", %{conn: conn} do + test "can assert title's exactness", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title", text: "PhoenixTest is the best!", exact: true) end - also_test_js "raises if title does not match expected value (Static)", %{conn: conn} do + test "raises if title does not match expected value (Static)", %{conn: conn} do msg = ignore_whitespace(""" Expected title to be "Not the title" but got "PhoenixTest is the best!" @@ -159,7 +157,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises if title does not match expected value (Live)", %{conn: conn} do + test "raises if title does not match expected value (Live)", %{conn: conn} do msg = ignore_whitespace(""" Expected title to be "Not the title" but got "PhoenixTest is the best!" @@ -172,8 +170,8 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises if title is contained but is not exactly the same as expected (with exact=true)", - %{conn: conn} do + test "raises if title is contained but is not exactly the same as expected (with exact=true)", + %{conn: conn} do msg = ignore_whitespace(""" Expected title to be "PhoenixTest" but got "PhoenixTest is the best!" @@ -186,7 +184,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises error if element cannot be found and selector matches a nested structure", %{ + test "raises error if element cannot be found and selector matches a nested structure", %{ conn: conn } do conn = visit(conn, "/page/index") @@ -215,7 +213,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "accepts a `count` option", %{conn: conn} do + test "accepts a `count` option", %{conn: conn} do conn |> visit("/page/index") |> assert_has(".multiple_links", count: 2) @@ -224,7 +222,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("h1", text: "Main page", count: 1) end - also_test_js "raises an error if count is more than expected count", %{conn: conn} do + test "raises an error if count is more than expected count", %{conn: conn} do session = visit(conn, "/page/index") msg = @@ -239,7 +237,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises an error if count is less than expected count", %{conn: conn} do + test "raises an error if count is less than expected count", %{conn: conn} do session = visit(conn, "/page/index") msg = @@ -254,14 +252,14 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "accepts an `exact` option to match text exactly", %{conn: conn} do + test "accepts an `exact` option to match text exactly", %{conn: conn} do conn |> visit("/page/index") |> assert_has("h1", text: "Main", exact: false) |> assert_has("h1", text: "Main page", exact: true) end - also_test_js "raises if `exact` text doesn't match", %{conn: conn} do + test "raises if `exact` text doesn't match", %{conn: conn} do msg = ignore_whitespace(""" Could not find any elements with selector "h1" and text "Main". @@ -290,7 +288,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("#multiple-items li", at: 2, text: "Legolas") end - also_test_js "raises if it cannot find element at `at` position", %{conn: conn} do + test "raises if it cannot find element at `at` position", %{conn: conn} do msg = ignore_whitespace(""" Could not find any elements with selector "#multiple-items li" and text "Aragorn" at position 2 @@ -305,28 +303,28 @@ defmodule PhoenixTest.AssertionsTest do end describe "refute_has/2" do - also_test_js "succeeds if no element is found with CSS selector (Static)", %{conn: conn} do + test "succeeds if no element is found with CSS selector (Static)", %{conn: conn} do conn |> visit("/page/index") |> refute_has("#some-invalid-id") |> refute_has("[data-role='invalid-role']") end - also_test_js "succeeds if no element is found with CSS selector (Live)", %{conn: conn} do + test "succeeds if no element is found with CSS selector (Live)", %{conn: conn} do conn |> visit("/live/index") |> refute_has("#some-invalid-id") |> refute_has("[data-role='invalid-role']") end - also_test_js "can refute presence of title (Static)", %{conn: conn} do + test "can refute presence of title (Static)", %{conn: conn} do conn |> visit("/page/index_no_layout") |> refute_has("title") |> refute_has("#something-else-to-test-pipe") end - also_test_js "accepts a `count` option", %{conn: conn} do + test "accepts a `count` option", %{conn: conn} do conn |> visit("/page/index") |> refute_has("h1", count: 2) @@ -335,7 +333,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has(".multiple_links", text: "Multiple links", count: 1) end - also_test_js "raises if element is found", %{conn: conn} do + test "raises if element is found", %{conn: conn} do msg = ignore_whitespace(""" Expected not to find any elements with selector "h1". @@ -354,7 +352,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises if title is found", %{conn: conn} do + test "raises if title is found", %{conn: conn} do msg = ignore_whitespace(""" Expected title not to be present but found: "PhoenixTest is the best!" @@ -367,7 +365,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises an error if multiple elements are found", %{conn: conn} do + test "raises an error if multiple elements are found", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -382,7 +380,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises if there is one element and count is 1", %{conn: conn} do + test "raises if there is one element and count is 1", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -395,7 +393,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises if there are the same number of elements as refuted", %{conn: conn} do + test "raises if there are the same number of elements as refuted", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -412,27 +410,27 @@ defmodule PhoenixTest.AssertionsTest do end describe "refute_has/3" do - also_test_js "can be used to refute on page title (Static)", %{conn: conn} do + test "can be used to refute on page title (Static)", %{conn: conn} do conn |> visit("/page/index") |> refute_has("title", text: "Not the title") |> refute_has("title", text: "Not this title either") end - also_test_js "can be used to refute on page title (Live)", %{conn: conn} do + test "can be used to refute on page title (Live)", %{conn: conn} do conn |> visit("/live/index") |> refute_has("title", text: "Not the title") |> refute_has("title", text: "Not this title either") end - also_test_js "can be used to refute page title's exactness", %{conn: conn} do + test "can be used to refute page title's exactness", %{conn: conn} do conn |> visit("/live/index") |> refute_has("title", text: "PhoenixTest is the", exact: true) end - also_test_js "raises if title matches value (Static)", %{conn: conn} do + test "raises if title matches value (Static)", %{conn: conn} do msg = ignore_whitespace(""" Expected title not to be "PhoenixTest is the best!" @@ -445,7 +443,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises if title matches value (Live)", %{conn: conn} do + test "raises if title matches value (Live)", %{conn: conn} do msg = ignore_whitespace(""" Expected title not to be "PhoenixTest is the best!" @@ -458,7 +456,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "succeeds if no element is found with CSS selector and text (Static)", %{conn: conn} do + test "succeeds if no element is found with CSS selector and text (Static)", %{conn: conn} do conn |> visit("/page/index") |> refute_has("h1", text: "Not main page") @@ -467,7 +465,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has("#title", text: "Not main page") end - also_test_js "succeeds if no element is found with CSS selector and text (Live)", %{conn: conn} do + test "succeeds if no element is found with CSS selector and text (Live)", %{conn: conn} do conn |> visit("/live/index") |> refute_has("h1", text: "Not main page") @@ -476,7 +474,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has("#title", text: "Not main page") end - also_test_js "raises an error if one element is found", %{conn: conn} do + test "raises an error if one element is found", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -495,7 +493,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises an error if multiple elements are found", %{conn: conn} do + test "raises an error if multiple elements are found", %{conn: conn} do conn = visit(conn, "/page/index") msg = @@ -518,13 +516,13 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "accepts an `exact` option to match text exactly", %{conn: conn} do + test "accepts an `exact` option to match text exactly", %{conn: conn} do conn |> visit("/page/index") |> refute_has("h1", text: "Main", exact: true) end - also_test_js "raises if `exact` text makes refutation false", %{conn: conn} do + test "raises if `exact` text makes refutation false", %{conn: conn} do msg = ignore_whitespace(""" Expected not to find any elements with selector "h1" and text "Main". @@ -543,13 +541,13 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "accepts an `at` option (without text) to refute on a specific element", %{conn: conn} do + test "accepts an `at` option (without text) to refute on a specific element", %{conn: conn} do conn |> visit("/page/index") |> refute_has("#single-list-item li", at: 2) end - also_test_js "accepts an `at` option with text to refute on a specific element", %{conn: conn} do + test "accepts an `at` option with text to refute on a specific element", %{conn: conn} do conn |> visit("/page/index") |> refute_has("#multiple-items li", at: 2, text: "Aragorn") @@ -580,25 +578,25 @@ defmodule PhoenixTest.AssertionsTest do end describe "assert_path" do - also_test_js "asserts the session's current path" do + test "asserts the session's current path" do session = %Live{current_path: "/page/index"} assert_path(session, "/page/index") end - also_test_js "asserts query params are the same" do + test "asserts query params are the same" do session = %Live{current_path: "/page/index?hello=world"} assert_path(session, "/page/index", query_params: %{"hello" => "world"}) end - also_test_js "order of query params does not matter" do + test "order of query params does not matter" do session = %Live{current_path: "/page/index?hello=world&foo=bar"} assert_path(session, "/page/index", query_params: %{"foo" => "bar", "hello" => "world"}) end - also_test_js "raises helpful error if path doesn't match" do + test "raises helpful error if path doesn't match" do msg = ignore_whitespace(""" Expected path to be "/page/not-index" but got "/page/index" @@ -611,7 +609,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises helpful error if path doesn't have query params" do + test "raises helpful error if path doesn't have query params" do msg = ignore_whitespace(""" Expected query params to be "details=true&foo=bar" but got nil @@ -624,7 +622,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises helpful error if query params don't match" do + test "raises helpful error if query params don't match" do msg = ignore_whitespace(""" Expected query params to be "goodbye=world&hi=bye" but got "hello=world&hi=bye" @@ -639,19 +637,19 @@ defmodule PhoenixTest.AssertionsTest do end describe "refute_path" do - also_test_js "refute the given path is the current path" do + test "refute the given path is the current path" do session = %Live{current_path: "/page/index"} refute_path(session, "/page/page_2") end - also_test_js "refutes query params are the same" do + test "refutes query params are the same" do session = %Live{current_path: "/page/index?hello=world"} refute_path(session, "/page/index", query_params: %{"hello" => "not-world"}) end - also_test_js "raises helpful error if path matches" do + test "raises helpful error if path matches" do msg = ignore_whitespace(""" Expected path not to be "/page/index" @@ -664,7 +662,7 @@ defmodule PhoenixTest.AssertionsTest do end end - also_test_js "raises helpful error if query params MATCH" do + test "raises helpful error if query params MATCH" do msg = ignore_whitespace(""" Expected query params not to be "hello=world&hi=bye" diff --git a/test/phoenix_test/live_test.exs b/test/phoenix_test/live_test.exs index e8b9e317..75de2bd8 100644 --- a/test/phoenix_test/live_test.exs +++ b/test/phoenix_test/live_test.exs @@ -1,21 +1,18 @@ defmodule PhoenixTest.LiveTest do - use PhoenixTest.Case, async: true + use PhoenixTest.Case, async: true, parameterize: [%{playwright: false}, %{playwright: true}] import PhoenixTest - import PhoenixTest.TestHelpers alias PhoenixTest.Driver - @moduletag :playwright - describe "assert_has/2 title" do - also_test_js "renders the page title", %{conn: conn} do + test "renders the page title", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title", text: "PhoenixTest is the best!") end - also_test_js "renders updated page title", %{conn: conn} do + test "renders updated page title", %{conn: conn} do conn |> visit("/live/index") |> click_button("Change page title") @@ -24,7 +21,7 @@ defmodule PhoenixTest.LiveTest do end describe "refute_has/1 title" do - also_test_js "returns nil if page title isn't found", %{conn: conn} do + test "returns nil if page title isn't found", %{conn: conn} do conn |> visit("/live/index_no_layout") |> refute_has("title") @@ -32,19 +29,19 @@ defmodule PhoenixTest.LiveTest do end describe "visit/2" do - also_test_js "navigates to given LiveView page", %{conn: conn} do + test "navigates to given LiveView page", %{conn: conn} do conn |> visit("/live/index") |> assert_has("h1", text: "LiveView main page") end - also_test_js "follows redirects", %{conn: conn} do + test "follows redirects", %{conn: conn} do conn |> visit("/live/redirect_on_mount/redirect") |> assert_has("h1", text: "LiveView main page") end - also_test_js "follows push redirects (push navigate)", %{conn: conn} do + test "follows push redirects (push navigate)", %{conn: conn} do conn |> visit("/live/redirect_on_mount/push_navigate") |> assert_has("h1", text: "LiveView main page") @@ -70,35 +67,35 @@ defmodule PhoenixTest.LiveTest do end describe "click_link/2" do - also_test_js "follows 'navigate' links", %{conn: conn} do + test "follows 'navigate' links", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate link") |> assert_has("h1", text: "LiveView page 2") end - also_test_js "follows navigation that subsequently redirect", %{conn: conn} do + test "follows navigation that subsequently redirect", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate (and redirect back) link") |> assert_has("h1", text: "LiveView main page") end - also_test_js "accepts click_link with selector", %{conn: conn} do + test "accepts click_link with selector", %{conn: conn} do conn |> visit("/live/index") |> click_link("a", "Navigate link") |> assert_has("h1", text: "LiveView page 2") end - also_test_js "handles patches to current view", %{conn: conn} do + test "handles patches to current view", %{conn: conn} do conn |> visit("/live/index") |> click_link("Patch link") |> assert_has("h2", text: "LiveView main page details") end - also_test_js "handles navigation to a non-liveview", %{conn: conn} do + test "handles navigation to a non-liveview", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate to non-liveview") @@ -129,7 +126,7 @@ defmodule PhoenixTest.LiveTest do # Playwright: Errors with PhoenixTest message (not Phoenix.LiveViewTest message) @tag playwright: false, reason: :different_error_message - also_test_js "raises an error when link element can't be found with given text", %{conn: conn} do + test "raises an error when link element can't be found with given text", %{conn: conn} do assert_raise ArgumentError, ~r/elements but none matched the text filter "No link"/, fn -> conn |> visit("/live/index") @@ -149,7 +146,7 @@ defmodule PhoenixTest.LiveTest do end describe "click_button/2" do - also_test_js "handles a `phx-click` button", %{conn: conn} do + test "handles a `phx-click` button", %{conn: conn} do conn |> visit("/live/index") |> click_button("Show tab") @@ -178,7 +175,7 @@ defmodule PhoenixTest.LiveTest do refute PhoenixTest.ActiveForm.active?(session.active_form) end - also_test_js "includes name and value if specified", %{conn: conn} do + test "includes name and value if specified", %{conn: conn} do conn |> visit("/live/index") |> fill_in("User Name", with: "Aragorn") @@ -186,7 +183,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:no-phx-change-form-button: save") end - also_test_js "includes default data if form is untouched", %{conn: conn} do + test "includes default data if form is untouched", %{conn: conn} do conn |> visit("/live/index") |> click_button("Save Full Form") @@ -198,7 +195,7 @@ defmodule PhoenixTest.LiveTest do |> refute_has("#form-data", text: "disabled_textarea:") end - also_test_js "can click button that does not submit form after filling form", %{conn: conn} do + test "can click button that does not submit form after filling form", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -208,7 +205,7 @@ defmodule PhoenixTest.LiveTest do |> refute_has("#form-data", text: "email: some@example.com") end - also_test_js "submits owner form if button isn't nested inside form (including button data)", %{ + test "submits owner form if button isn't nested inside form (including button data)", %{ conn: conn } do conn @@ -221,7 +218,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "form-button: save-owner-form") end - also_test_js "follows form's redirect to live page", %{conn: conn} do + test "follows form's redirect to live page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form", &fill_in(&1, "Name", with: "Aragorn")) @@ -229,7 +226,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("h1", text: "LiveView page 2") end - also_test_js "follows form's redirect to static page", %{conn: conn} do + test "follows form's redirect to static page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form-to-static", &fill_in(&1, "Name", with: "Aragorn")) @@ -237,7 +234,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("h1", text: "Main page") end - also_test_js "submits regular (non phx-submit) form", %{conn: conn} do + test "submits regular (non phx-submit) form", %{conn: conn} do conn |> visit("/live/index") |> within("#non-liveview-form", &fill_in(&1, "Name", with: "Aragorn")) @@ -257,7 +254,7 @@ defmodule PhoenixTest.LiveTest do end end - also_test_js "raises an error when there are no buttons on page", %{conn: conn} do + test "raises an error when there are no buttons on page", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find an element/, fn -> conn |> visit("/live/page_2") @@ -276,7 +273,7 @@ defmodule PhoenixTest.LiveTest do end end - also_test_js "raises an error if active form but can't find button", %{conn: conn} do + test "raises an error if active form but can't find button", %{conn: conn} do msg = ~r/Could not find an element/ assert_raise ArgumentError, msg, fn -> @@ -292,7 +289,7 @@ defmodule PhoenixTest.LiveTest do end describe "within/3" do - also_test_js "scopes assertions within selector", %{conn: conn} do + test "scopes assertions within selector", %{conn: conn} do conn |> visit("/live/index") |> assert_has("button", text: "Reset") @@ -301,7 +298,7 @@ defmodule PhoenixTest.LiveTest do end) end - also_test_js "scopes further form actions within a selector", %{conn: conn} do + test "scopes further form actions within a selector", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -310,7 +307,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "email: someone@example.com") end - also_test_js "raises when data is not in scoped HTML", %{conn: conn} do + test "raises when data is not in scoped HTML", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with label "User Name"/, fn -> conn |> visit("/live/index") @@ -322,7 +319,7 @@ defmodule PhoenixTest.LiveTest do end describe "fill_in/4" do - also_test_js "fills in a single text field based on the label", %{conn: conn} do + test "fills in a single text field based on the label", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -331,7 +328,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "email: someone@example.com") end - also_test_js "can fill input with `nil` to override existing value", %{conn: conn} do + test "can fill input with `nil` to override existing value", %{conn: conn} do conn |> visit("/live/index") |> within("#pre-rendered-data-form", fn session -> @@ -340,7 +337,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "input's value is empty") end - also_test_js "can fill-in textareas", %{conn: conn} do + test "can fill-in textareas", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Notes", with: "Dunedain. Heir to the throne. King of Arnor and Gondor") @@ -350,7 +347,7 @@ defmodule PhoenixTest.LiveTest do ) end - also_test_js "can fill-in complex form fields", %{conn: conn} do + test "can fill-in complex form fields", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Aragorn") @@ -362,7 +359,7 @@ defmodule PhoenixTest.LiveTest do ) end - also_test_js "can fill in numbers", %{conn: conn} do + test "can fill in numbers", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Level (number)", with: 10) @@ -370,7 +367,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "level: 10") end - also_test_js "works in 'nested' forms", %{conn: conn} do + test "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> fill_in("User Name", with: "Aragorn") @@ -380,7 +377,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:role: El Jefe") end - also_test_js "can be used to submit form", %{conn: conn} do + test "can be used to submit form", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -390,7 +387,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "email: someone@example.com") end - also_test_js "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do + test "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -401,7 +398,7 @@ defmodule PhoenixTest.LiveTest do |> refute_has("#form-data", text: "email: frodo@example.com") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -410,7 +407,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "name: Frodo") end - also_test_js "can target input with selector if multiple labels have same text", %{conn: conn} do + test "can target input with selector if multiple labels have same text", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -419,7 +416,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "book-characters: Frodo") end - also_test_js "raises an error when element can't be found with label", %{conn: conn} do + test "raises an error when element can't be found with label", %{conn: conn} do msg = ~r/Could not find element with label "Non-existent Email Label"./ assert_raise ArgumentError, msg, fn -> @@ -429,7 +426,7 @@ defmodule PhoenixTest.LiveTest do end end - also_test_js "raises an error when label is found but no corresponding input is found", %{conn: conn} do + test "raises an error when label is found but no corresponding input is found", %{conn: conn} do msg = ~r/Found label but can't find labeled element whose `id` matches/ assert_raise ArgumentError, msg, fn -> @@ -441,21 +438,21 @@ defmodule PhoenixTest.LiveTest do end describe "select/3" do - also_test_js "selects given option for a label", %{conn: conn} do + test "selects given option for a label", %{conn: conn} do conn |> visit("/live/index") |> select("Elf", from: "Race") |> assert_has("#full-form option[value='elf']") end - also_test_js "allows selecting option if a similar option exists", %{conn: conn} do + test "allows selecting option if a similar option exists", %{conn: conn} do conn |> visit("/live/index") |> select("Orc", from: "Race") |> assert_has("#full-form option[value='orc']") end - also_test_js "works in 'nested' forms", %{conn: conn} do + test "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> select("False", from: "User Admin") @@ -463,7 +460,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:admin: false") end - also_test_js "can be used to submit form", %{conn: conn} do + test "can be used to submit form", %{conn: conn} do conn |> visit("/live/index") |> select("Elf", from: "Race") @@ -471,7 +468,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "race: elf") end - also_test_js "works for multiple select", %{conn: conn} do + test "works for multiple select", %{conn: conn} do conn |> visit("/live/index") |> select("Elf", from: "Race") @@ -480,7 +477,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "[elf, dwarf]") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -500,7 +497,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "race: dwarf") end - also_test_js "can target option with selector if multiple labels have same text", %{conn: conn} do + test "can target option with selector if multiple labels have same text", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -520,7 +517,7 @@ defmodule PhoenixTest.LiveTest do end describe "check/3" do - also_test_js "checks a checkbox", %{conn: conn} do + test "checks a checkbox", %{conn: conn} do conn |> visit("/live/index") |> check("Admin") @@ -528,7 +525,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: on") end - also_test_js "can check an unchecked checkbox", %{conn: conn} do + test "can check an unchecked checkbox", %{conn: conn} do conn |> visit("/live/index") |> uncheck("Admin") @@ -537,7 +534,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: on") end - also_test_js "handle checkbox name with '?'", %{conn: conn} do + test "handle checkbox name with '?'", %{conn: conn} do conn |> visit("/live/index") |> check("Subscribe") @@ -545,7 +542,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "subscribe?: on") end - also_test_js "works in 'nested' forms", %{conn: conn} do + test "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> check("Payer") @@ -553,7 +550,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:payer: on") end - also_test_js "works with phx-click outside a form", %{conn: conn} do + test "works with phx-click outside a form", %{conn: conn} do conn |> visit("/live/index") |> within("#not-a-form", fn session -> @@ -562,7 +559,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "value: second-breakfast") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -571,7 +568,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "human: yes") end - also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -591,7 +588,7 @@ defmodule PhoenixTest.LiveTest do end describe "uncheck/3" do - also_test_js "sends the default value (in hidden input)", %{conn: conn} do + test "sends the default value (in hidden input)", %{conn: conn} do conn |> visit("/live/index") |> uncheck("Admin") @@ -599,7 +596,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: off") end - also_test_js "can uncheck a previous check/2 in the test", %{conn: conn} do + test "can uncheck a previous check/2 in the test", %{conn: conn} do conn |> visit("/live/index") |> check("Admin") @@ -608,7 +605,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "admin: off") end - also_test_js "works in 'nested' forms", %{conn: conn} do + test "works in 'nested' forms", %{conn: conn} do conn |> visit("/live/index") |> check("Payer") @@ -617,7 +614,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "user:payer: off") end - also_test_js "works with phx-click outside a form", %{conn: conn} do + test "works with phx-click outside a form", %{conn: conn} do conn |> visit("/live/index") |> within("#not-a-form", fn session -> @@ -628,7 +625,7 @@ defmodule PhoenixTest.LiveTest do |> refute_has("#form-data", text: "value: second-breakfast") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -639,7 +636,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "human: no") end - also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -662,7 +659,7 @@ defmodule PhoenixTest.LiveTest do end describe "choose/3" do - also_test_js "chooses an option in radio button", %{conn: conn} do + test "chooses an option in radio button", %{conn: conn} do conn |> visit("/live/index") |> choose("Email Choice") @@ -670,14 +667,14 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "contact: email") end - also_test_js "uses the default 'checked' if present", %{conn: conn} do + test "uses the default 'checked' if present", %{conn: conn} do conn |> visit("/live/index") |> click_button("Save Full Form") |> assert_has("#form-data", text: "contact: mail") end - also_test_js "works with a phx-click outside of a form", %{conn: conn} do + test "works with a phx-click outside of a form", %{conn: conn} do conn |> visit("/live/index") |> within("#not-a-form", fn session -> @@ -686,7 +683,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "value: dewey") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -695,7 +692,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "book-or-movie: book") end - also_test_js "can specify input selector when multiple options have same label in same form", %{ + test "can specify input selector when multiple options have same label in same form", %{ conn: conn } do conn @@ -717,7 +714,7 @@ defmodule PhoenixTest.LiveTest do end describe "upload/4" do - also_test_js "uploads an image", %{conn: conn} do + test "uploads an image", %{conn: conn} do conn |> visit("/live/index") |> within("#full-form", fn session -> @@ -728,7 +725,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "avatar: elixir.jpg") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/live/index") |> within("#complex-labels", fn session -> @@ -739,7 +736,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "avatar: elixir.jpg") end - also_test_js "can specify input selector when multiple inputs have same label", %{conn: conn} do + test "can specify input selector when multiple inputs have same label", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -767,7 +764,7 @@ defmodule PhoenixTest.LiveTest do end describe "filling out full form with field functions" do - also_test_js "populates all fields", %{conn: conn} do + test "populates all fields", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Legolas") @@ -785,7 +782,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "notes: Woodland Elf") end - also_test_js "populates all fields in nested forms", %{conn: conn} do + test "populates all fields in nested forms", %{conn: conn} do conn |> visit("/live/index") |> fill_in("User Name", with: "Legolas") @@ -797,7 +794,7 @@ defmodule PhoenixTest.LiveTest do end describe "submit/1" do - also_test_js "submits a pre-filled form via phx-submit", %{conn: conn} do + test "submits a pre-filled form via phx-submit", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -807,7 +804,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "email: some@example.com") end - also_test_js "includes pre-rendered data", %{conn: conn} do + test "includes pre-rendered data", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Aragorn") @@ -817,7 +814,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "contact: mail") end - also_test_js "includes the first button's name and value if present", %{conn: conn} do + test "includes the first button's name and value if present", %{conn: conn} do conn |> visit("/live/index") |> fill_in("First Name", with: "Aragorn") @@ -825,7 +822,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "full_form_button: save") end - also_test_js "can submit form without button", %{conn: conn} do + test "can submit form without button", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Country of Origin", with: "Arnor") @@ -833,7 +830,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "country: Arnor") end - also_test_js "follows form's redirect to live page", %{conn: conn} do + test "follows form's redirect to live page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form", fn session -> @@ -844,7 +841,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("h1", text: "LiveView page 2") end - also_test_js "follows form's redirect to static page", %{conn: conn} do + test "follows form's redirect to static page", %{conn: conn} do conn |> visit("/live/index") |> within("#redirect-form-to-static", fn session -> @@ -871,7 +868,7 @@ defmodule PhoenixTest.LiveTest do end) end - also_test_js "submits regular (non phx-submit) form", %{conn: conn} do + test "submits regular (non phx-submit) form", %{conn: conn} do conn |> visit("/live/index") |> within("#non-liveview-form", fn session -> @@ -956,47 +953,47 @@ defmodule PhoenixTest.LiveTest do end describe "assert_path" do - also_test_js "it is set on visit", %{conn: conn} do + test "it is set on visit", %{conn: conn} do conn |> visit("/live/index") |> assert_path("/live/index") end - also_test_js "it is set on visit with query string", %{conn: conn} do + test "it is set on visit with query string", %{conn: conn} do conn |> visit("/live/index?foo=bar") |> assert_path("/live/index", query_params: %{foo: "bar"}) end - also_test_js "it is updated on href navigation", %{conn: conn} do + test "it is updated on href navigation", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate to non-liveview") |> assert_path("/page/index", query_params: %{details: "true", foo: "bar"}) end - also_test_js "it is updated on live navigation", %{conn: conn} do + test "it is updated on live navigation", %{conn: conn} do conn |> visit("/live/index") |> click_link("Navigate link") |> assert_path("/live/page_2", query_params: %{details: "true", foo: "bar"}) end - also_test_js "it is updated on live patching", %{conn: conn} do + test "it is updated on live patching", %{conn: conn} do conn |> visit("/live/index") |> click_link("Patch link") |> assert_path("/live/index", query_params: %{details: "true", foo: "bar"}) end - also_test_js "it is updated on push navigation", %{conn: conn} do + test "it is updated on push navigation", %{conn: conn} do conn |> visit("/live/index") |> click_button("Button with push navigation") |> assert_path("/live/page_2", query_params: %{foo: "bar"}) end - also_test_js "it is updated on push patch", %{conn: conn} do + test "it is updated on push patch", %{conn: conn} do conn |> visit("/live/index") |> click_button("Button with push patch") @@ -1005,7 +1002,7 @@ defmodule PhoenixTest.LiveTest do end describe "shared form helpers behavior" do - also_test_js "triggers phx-change validations", %{conn: conn} do + test "triggers phx-change validations", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -1016,7 +1013,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-errors", text: "Errors present") end - also_test_js "sends _target with phx-change events", %{conn: conn} do + test "sends _target with phx-change events", %{conn: conn} do conn |> visit("/live/index") |> within("#email-form", fn session -> @@ -1040,14 +1037,14 @@ defmodule PhoenixTest.LiveTest do assert starting_html == ending_html end - also_test_js "follows redirects on phx-change", %{conn: conn} do + test "follows redirects on phx-change", %{conn: conn} do conn |> visit("/live/index") |> fill_in("Email with redirect", with: "someone@example.com") |> assert_has("h1", text: "LiveView page 2") end - also_test_js "preserves correct order of active form vs form data", %{conn: conn} do + test "preserves correct order of active form vs form data", %{conn: conn} do conn |> visit("/live/index") |> within("#changes-hidden-input-form", fn session -> diff --git a/test/phoenix_test/playwright_test.exs b/test/phoenix_test/playwright_test.exs index 81d205da..42b6cd6b 100644 --- a/test/phoenix_test/playwright_test.exs +++ b/test/phoenix_test/playwright_test.exs @@ -1,17 +1,19 @@ defmodule PhoenixTest.PlaywrightTest do - use PhoenixTest.Case, async: true + use PhoenixTest.Case, + async: true, + parameterize: Enum.map(~w(chromium firefox)a, &%{playwright: [browser: &1]}) import PhoenixTest - @moduletag :playwright - describe "render_page_title/1" do - test "uses playwright driver by default", %{conn: conn} do - session = visit(conn, "/live/index") - assert %PhoenixTest.Playwright{} = session + unless System.version() in ~w(1.15.0 1.16.0 1.17.0) do + test "runs in multiple browsers via ExUnit `parameterize`", %{conn: conn} do + session = visit(conn, "/live/index") + assert %PhoenixTest.Playwright{} = session - title = PhoenixTest.Driver.render_page_title(session) - assert title == "PhoenixTest is the best!" + title = PhoenixTest.Driver.render_page_title(session) + assert title == "PhoenixTest is the best!" + end end @tag playwright: false diff --git a/test/phoenix_test/static_test.exs b/test/phoenix_test/static_test.exs index ff567926..a20a9dbb 100644 --- a/test/phoenix_test/static_test.exs +++ b/test/phoenix_test/static_test.exs @@ -1,13 +1,11 @@ defmodule PhoenixTest.StaticTest do - use PhoenixTest.Case, async: true + use PhoenixTest.Case, async: true, parameterize: [%{playwright: false}, %{playwright: true}] import PhoenixTest import PhoenixTest.TestHelpers - @moduletag :playwright - describe "render_page_title/1" do - also_test_js "renders the page title", %{conn: conn} do + test "renders the page title", %{conn: conn} do title = conn |> visit("/page/index") @@ -16,7 +14,7 @@ defmodule PhoenixTest.StaticTest do assert title == "PhoenixTest is the best!" end - also_test_js "renders nil if there's no page title", %{conn: conn} do + test "renders nil if there's no page title", %{conn: conn} do title = conn |> visit("/page/index_no_layout") @@ -27,13 +25,13 @@ defmodule PhoenixTest.StaticTest do end describe "visit/2" do - also_test_js "navigates to given static page", %{conn: conn} do + test "navigates to given static page", %{conn: conn} do conn |> visit("/page/index") |> assert_has("h1", text: "Main page") end - also_test_js "follows redirects", %{conn: conn} do + test "follows redirects", %{conn: conn} do conn |> visit("/page/redirect_to_static") |> assert_has("h1", text: "Main page") @@ -59,21 +57,21 @@ defmodule PhoenixTest.StaticTest do end describe "click_link/2" do - also_test_js "follows link's path", %{conn: conn} do + test "follows link's path", %{conn: conn} do conn |> visit("/page/index") |> click_link("Page 2") |> assert_has("h1", text: "Page 2") end - also_test_js "follows link that subsequently redirects", %{conn: conn} do + test "follows link that subsequently redirects", %{conn: conn} do conn |> visit("/page/index") |> click_link("Navigate away and redirect back") |> assert_has("h1", text: "Main page") end - also_test_js "accepts selector for link", %{conn: conn} do + test "accepts selector for link", %{conn: conn} do conn |> visit("/page/index") |> click_link("a", "Page 2") @@ -92,7 +90,7 @@ defmodule PhoenixTest.StaticTest do end) end - also_test_js "handles navigation to a LiveView", %{conn: conn} do + test "handles navigation to a LiveView", %{conn: conn} do conn |> visit("/page/index") |> click_link("To LiveView!") @@ -139,7 +137,7 @@ defmodule PhoenixTest.StaticTest do end end - also_test_js "raises error when there are multiple links with same text", %{conn: conn} do + test "raises error when there are multiple links with same text", %{conn: conn} do assert_raise ArgumentError, ~r/Found more than one element with selector/, fn -> conn |> visit("/page/index") @@ -147,7 +145,7 @@ defmodule PhoenixTest.StaticTest do end end - also_test_js "raises an error when link element can't be found with given text", %{conn: conn} do + test "raises an error when link element can't be found with given text", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with selector/, fn -> conn |> visit("/page/index") @@ -155,7 +153,7 @@ defmodule PhoenixTest.StaticTest do end end - also_test_js "raises an error when there are no links on the page", %{conn: conn} do + test "raises an error when there are no links on the page", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with selector/, fn -> conn |> visit("/page/page_2") @@ -165,35 +163,35 @@ defmodule PhoenixTest.StaticTest do end describe "click_button/2" do - also_test_js "handles a button that defaults to GET", %{conn: conn} do + test "handles a button that defaults to GET", %{conn: conn} do conn |> visit("/page/index") |> click_button("Get record") |> assert_has("h1", text: "Record received") end - also_test_js "accepts selector for button", %{conn: conn} do + test "accepts selector for button", %{conn: conn} do conn |> visit("/page/index") |> click_button("button", "Get record") |> assert_has("h1", text: "Record received") end - also_test_js "handles a button clicks when button PUTs data (hidden input)", %{conn: conn} do + test "handles a button clicks when button PUTs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Mark as active") |> assert_has("h1", text: "Record updated") end - also_test_js "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do + test "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Delete record") |> assert_has("h1", text: "Record deleted") end - also_test_js "can submit forms with input type submit", %{conn: conn} do + test "can submit forms with input type submit", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "sample@example.com") @@ -201,7 +199,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "email: sample@example.com") end - also_test_js "can handle clicking button that does not submit form after filling a form", %{conn: conn} do + test "can handle clicking button that does not submit form after filling a form", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "some@example.com") @@ -209,7 +207,7 @@ defmodule PhoenixTest.StaticTest do |> refute_has("#form-data", text: "email: some@example.com") end - also_test_js "submits owner form if button isn't nested inside form", %{conn: conn} do + test "submits owner form if button isn't nested inside form", %{conn: conn} do conn |> visit("/page/index") |> within("#owner-form", fn session -> @@ -219,14 +217,14 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Aragorn") end - also_test_js "can handle redirects to a LiveView", %{conn: conn} do + test "can handle redirects to a LiveView", %{conn: conn} do conn |> visit("/page/index") |> click_button("Post and Redirect") |> assert_has("h1", text: "LiveView main page") end - also_test_js "handles form submission via `data-method` & `data-to` attributes", %{conn: conn} do + test "handles form submission via `data-method` & `data-to` attributes", %{conn: conn} do conn |> visit("/page/index") |> click_button("Data-method Delete") @@ -255,7 +253,7 @@ defmodule PhoenixTest.StaticTest do refute PhoenixTest.ActiveForm.active?(session.active_form) end - also_test_js "includes name and value if specified", %{conn: conn} do + test "includes name and value if specified", %{conn: conn} do conn |> visit("/page/index") |> fill_in("User Name", with: "Aragorn") @@ -263,7 +261,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:save-button: nested-form-save") end - also_test_js "can handle clicking button that does not submit form after fill_in", %{conn: conn} do + test "can handle clicking button that does not submit form after fill_in", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "some@example.com") @@ -271,7 +269,7 @@ defmodule PhoenixTest.StaticTest do |> refute_has("#form-data", text: "email: some@example.com") end - also_test_js "includes default data if form is untouched", %{conn: conn} do + test "includes default data if form is untouched", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") @@ -294,7 +292,7 @@ defmodule PhoenixTest.StaticTest do end end - also_test_js "raises an error when there are no buttons on page", %{conn: conn} do + test "raises an error when there are no buttons on page", %{conn: conn} do msg = ~r/Could not find an element with given selectors/ assert_raise ArgumentError, msg, fn -> @@ -304,7 +302,7 @@ defmodule PhoenixTest.StaticTest do end end - also_test_js "raises an error if can't find button", %{conn: conn} do + test "raises an error if can't find button", %{conn: conn} do msg = ~r/Could not find an element with given selectors/ assert_raise ArgumentError, msg, fn -> @@ -328,7 +326,7 @@ defmodule PhoenixTest.StaticTest do end describe "within/3" do - also_test_js "scopes assertions within selector", %{conn: conn} do + test "scopes assertions within selector", %{conn: conn} do conn |> visit("/page/index") |> assert_has("button", text: "Get record") @@ -337,7 +335,7 @@ defmodule PhoenixTest.StaticTest do end) end - also_test_js "scopes further form actions within a selector", %{conn: conn} do + test "scopes further form actions within a selector", %{conn: conn} do conn |> visit("/page/index") |> within("#email-form", fn session -> @@ -348,7 +346,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "email: someone@example.com") end - also_test_js "raises when data is not in scoped HTML", %{conn: conn} do + test "raises when data is not in scoped HTML", %{conn: conn} do assert_raise ArgumentError, ~r/Could not find element with label "User Name"/, fn -> conn |> visit("/page/index") @@ -360,7 +358,7 @@ defmodule PhoenixTest.StaticTest do end describe "fill_in/4" do - also_test_js "fills in a single text field based on the label", %{conn: conn} do + test "fills in a single text field based on the label", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Email", with: "someone@example.com") @@ -368,7 +366,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "email: someone@example.com") end - also_test_js "can fill input with `nil` to override existing value", %{conn: conn} do + test "can fill input with `nil` to override existing value", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Pre Rendered Input", with: nil) @@ -376,7 +374,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "input's value is empty") end - also_test_js "can fill-in complex form fields", %{conn: conn} do + test "can fill-in complex form fields", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -388,7 +386,7 @@ defmodule PhoenixTest.StaticTest do ) end - also_test_js "can fill in numbers", %{conn: conn} do + test "can fill in numbers", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Level (number)", with: 10) @@ -396,7 +394,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "level: 10") end - also_test_js "works in 'nested' forms", %{conn: conn} do + test "works in 'nested' forms", %{conn: conn} do conn |> visit("/page/index") |> fill_in("User Name", with: "Aragorn") @@ -407,7 +405,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:role: El Jefe") end - also_test_js "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do + test "can be combined with other forms' fill_ins (without pollution)", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -417,7 +415,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:name: Legolas") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -427,7 +425,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Frodo") end - also_test_js "can target input with selector if multiple labels have same text", %{conn: conn} do + test "can target input with selector if multiple labels have same text", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -437,7 +435,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "book-characters: Frodo") end - also_test_js "raises an error when element can't be found with label", %{conn: conn} do + test "raises an error when element can't be found with label", %{conn: conn} do msg = ~r/Could not find element with label "Non-existent Email Label"./ assert_raise ArgumentError, msg, fn -> @@ -447,7 +445,7 @@ defmodule PhoenixTest.StaticTest do end end - also_test_js "raises an error when label is found but no corresponding input is found", %{conn: conn} do + test "raises an error when label is found but no corresponding input is found", %{conn: conn} do msg = ~r/Found label but can't find labeled element whose `id` matches/ assert_raise ArgumentError, msg, fn -> @@ -459,7 +457,7 @@ defmodule PhoenixTest.StaticTest do end describe "select/3" do - also_test_js "selects given option for a label", %{conn: conn} do + test "selects given option for a label", %{conn: conn} do conn |> visit("/page/index") |> select("Elf", from: "Race") @@ -467,21 +465,21 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race: elf") end - also_test_js "picks first by default", %{conn: conn} do + test "picks first by default", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") |> assert_has("#form-data", text: "race: human") end - also_test_js "allows selecting option if a similar option exists", %{conn: conn} do + test "allows selecting option if a similar option exists", %{conn: conn} do conn |> visit("/page/index") |> select("Orc", from: "Race") |> assert_has("#full-form option[value='orc']") end - also_test_js "works in 'nested' forms", %{conn: conn} do + test "works in 'nested' forms", %{conn: conn} do conn |> visit("/page/index") |> select("False", from: "User Admin") @@ -489,7 +487,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:admin: false") end - also_test_js "handles multi select", %{conn: conn} do + test "handles multi select", %{conn: conn} do conn |> visit("/page/index") |> select(["Elf", "Dwarf"], from: "Race 2") @@ -497,7 +495,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race_2: [elf,dwarf]") end - also_test_js "contains no data for empty multi select", %{conn: conn} do + test "contains no data for empty multi select", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") @@ -539,7 +537,7 @@ defmodule PhoenixTest.StaticTest do end describe "check/3" do - also_test_js "checks a checkbox", %{conn: conn} do + test "checks a checkbox", %{conn: conn} do conn |> visit("/page/index") |> check("Admin (boolean)") @@ -547,7 +545,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin_boolean: true") end - also_test_js "sets checkbox value as 'on' by default", %{conn: conn} do + test "sets checkbox value as 'on' by default", %{conn: conn} do conn |> visit("/page/index") |> check("Admin") @@ -555,7 +553,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: on") end - also_test_js "can check an unchecked checkbox", %{conn: conn} do + test "can check an unchecked checkbox", %{conn: conn} do conn |> visit("/page/index") |> uncheck("Admin") @@ -564,7 +562,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: on") end - also_test_js "handle checkbox name with '?'", %{conn: conn} do + test "handle checkbox name with '?'", %{conn: conn} do conn |> visit("/page/index") |> check("Subscribe") @@ -572,7 +570,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "subscribe?: on") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -582,7 +580,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "human: yes") end - also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -594,7 +592,7 @@ defmodule PhoenixTest.StaticTest do end describe "uncheck/3" do - also_test_js "sends the default value (in hidden input)", %{conn: conn} do + test "sends the default value (in hidden input)", %{conn: conn} do conn |> visit("/page/index") |> uncheck("Admin") @@ -602,7 +600,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: off") end - also_test_js "can uncheck a previous check/2 in the test", %{conn: conn} do + test "can uncheck a previous check/2 in the test", %{conn: conn} do conn |> visit("/page/index") |> check("Admin") @@ -611,7 +609,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "admin: off") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -623,7 +621,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "human: no") end - also_test_js "can specify input selector when multiple checkboxes have same label", %{conn: conn} do + test "can specify input selector when multiple checkboxes have same label", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -638,7 +636,7 @@ defmodule PhoenixTest.StaticTest do end describe "choose/3" do - also_test_js "chooses an option in radio button", %{conn: conn} do + test "chooses an option in radio button", %{conn: conn} do conn |> visit("/page/index") |> choose("Email Choice") @@ -646,14 +644,14 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "contact: email") end - also_test_js "uses the default 'checked' if present", %{conn: conn} do + test "uses the default 'checked' if present", %{conn: conn} do conn |> visit("/page/index") |> click_button("Save Full Form") |> assert_has("#form-data", text: "contact: mail") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -663,7 +661,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "book-or-movie: book") end - also_test_js "can specify input selector when multiple options have same label in same form", %{ + test "can specify input selector when multiple options have same label in same form", %{ conn: conn } do conn @@ -678,7 +676,7 @@ defmodule PhoenixTest.StaticTest do end describe "upload/4" do - also_test_js "uploads image", %{conn: conn} do + test "uploads image", %{conn: conn} do conn |> visit("/page/index") |> within("#file-upload-form", fn session -> @@ -701,7 +699,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "avatars:[]: phoenix.jpg") end - also_test_js "uploads an image in nested forms", %{conn: conn} do + test "uploads an image in nested forms", %{conn: conn} do conn |> visit("/page/index") |> upload("Nested Avatar", "test/files/elixir.jpg") @@ -709,7 +707,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "user:avatar: elixir.jpg") end - also_test_js "can target a label with exact: false", %{conn: conn} do + test "can target a label with exact: false", %{conn: conn} do conn |> visit("/page/index") |> within("#complex-labels", fn session -> @@ -734,7 +732,7 @@ defmodule PhoenixTest.StaticTest do end describe "filling out full form with field functions" do - also_test_js "populates all fields", %{conn: conn} do + test "populates all fields", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Legolas") @@ -752,7 +750,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "notes: Woodland Elf") end - also_test_js "populates all fields in nested forms", %{conn: conn} do + test "populates all fields in nested forms", %{conn: conn} do conn |> visit("/page/index") |> fill_in("User Name", with: "Legolas") @@ -764,7 +762,7 @@ defmodule PhoenixTest.StaticTest do end describe "submit/1" do - also_test_js "submits form even if no submit is present (acts as )", %{conn: conn} do + test "submits form even if no submit is present (acts as )", %{conn: conn} do conn |> visit("/page/index") |> within("#no-submit-button-form", fn session -> @@ -775,8 +773,8 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Aragorn") end - also_test_js "includes pre-rendered data (input value, selected option, checked checkbox, checked radio button)", - %{conn: conn} do + test "includes pre-rendered data (input value, selected option, checked checkbox, checked radio button)", + %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -785,7 +783,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race: human") end - also_test_js "includes the first button's name and value if present", %{conn: conn} do + test "includes the first button's name and value if present", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -793,7 +791,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "full_form_button: save") end - also_test_js "can submit form without button", %{conn: conn} do + test "can submit form without button", %{conn: conn} do conn |> visit("/page/index") |> fill_in("Country of Origin", with: "Arnor") @@ -801,7 +799,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "country: Arnor") end - also_test_js "updates current_path on submit", %{conn: conn} do + test "updates current_path on submit", %{conn: conn} do conn |> visit("/page/index") |> fill_in("First Name", with: "Aragorn") @@ -809,7 +807,7 @@ defmodule PhoenixTest.StaticTest do |> assert_path("/page/create_record") end - also_test_js "can handle redirects", %{conn: conn} do + test "can handle redirects", %{conn: conn} do conn |> visit("/page/index") |> within("#no-submit-button-and-redirect", fn session -> @@ -836,7 +834,7 @@ defmodule PhoenixTest.StaticTest do end) end - also_test_js "handles when form PUTs data through hidden input", %{conn: conn} do + test "handles when form PUTs data through hidden input", %{conn: conn} do conn |> visit("/page/index") |> within("#update-form", fn session -> @@ -847,7 +845,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "name: Aragorn") end - also_test_js "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do + test "handles a button clicks when button DELETEs data (hidden input)", %{conn: conn} do conn |> visit("/page/index") |> click_button("Delete record") @@ -889,7 +887,7 @@ defmodule PhoenixTest.StaticTest do %{open_fun: open_fun} end - also_test_js "opens the browser ", %{conn: conn, open_fun: open_fun} do + test "opens the browser ", %{conn: conn, open_fun: open_fun} do conn |> visit("/page/index") |> open_browser(open_fun) @@ -926,26 +924,26 @@ defmodule PhoenixTest.StaticTest do end describe "assert_path" do - also_test_js "it is set on visit", %{conn: conn} do + test "it is set on visit", %{conn: conn} do conn |> visit("/page/index") |> assert_path("/page/index") end - also_test_js "it includes query string if available", %{conn: conn} do + test "it includes query string if available", %{conn: conn} do conn |> visit("/page/index?foo=bar") |> assert_path("/page/index", query_params: %{foo: "bar"}) end - also_test_js "it is updated on href navigation", %{conn: conn} do + test "it is updated on href navigation", %{conn: conn} do conn |> visit("/page/index") |> click_link("Page 2") |> assert_path("/page/page_2", query_params: %{foo: "bar"}) end - also_test_js "it is updated on redirects", %{conn: conn} do + test "it is updated on redirects", %{conn: conn} do conn |> visit("/page/index") |> click_link("Navigate away and redirect back") diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex index 202139a7..a5906dc7 100644 --- a/test/support/test_helpers.ex +++ b/test/support/test_helpers.ex @@ -1,8 +1,6 @@ defmodule PhoenixTest.TestHelpers do @moduledoc false - require ExUnit.Case - @doc """ Converts a multi-line string into a whitespace-forgiving regex """ @@ -14,15 +12,4 @@ defmodule PhoenixTest.TestHelpers do |> Enum.map_join("\n", fn s -> "\\s*" <> s <> "\\s*" end) |> Regex.compile!([:dotall]) end - - defmacro also_test_js(message, var \\ quote(do: _), contents) do - quote location: :keep do - tags = Module.get_attribute(__MODULE__, :tag) - @tag playwright: false - ExUnit.Case.test(unquote(message), unquote(var), unquote(contents)) - - for tag <- tags, do: @tag(tag) - ExUnit.Case.test(unquote(message) <> " (Playwright)", unquote(var), unquote(contents)) - end - end end From 48bb144effe511a36bf51be15ebf5510cfbf8c9f Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Wed, 30 Oct 2024 12:27:15 +0100 Subject: [PATCH 3/6] Retry failed tests, retry with configurable timeout --- .github/workflows/ci.yml | 2 +- lib/phoenix_test/assertions.ex | 2 +- lib/phoenix_test/case.ex | 57 +++++++++++++-- lib/phoenix_test/playwright.ex | 88 ++++++++++++++++++++--- lib/phoenix_test/playwright/connection.ex | 62 ++++++++++++---- lib/phoenix_test/playwright/frame.ex | 10 ++- lib/phoenix_test/playwright/message.ex | 6 +- lib/phoenix_test/playwright/port.ex | 7 +- lib/phoenix_test/playwright/selector.ex | 34 +++++---- test/phoenix_test/assertions_test.exs | 2 +- test/phoenix_test/live_test.exs | 15 ++-- test/phoenix_test/playwright_test.exs | 2 - test/phoenix_test/static_test.exs | 9 ++- 13 files changed, 228 insertions(+), 68 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b97548d..2d353598 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,4 +79,4 @@ jobs: run: npm exec --prefix priv/static/assets playwright install --with-deps - name: Run tests - run: mix test + run: "mix test || if [[ $? = 2 ]]; then mix test --failed; else false; fi" diff --git a/lib/phoenix_test/assertions.ex b/lib/phoenix_test/assertions.ex index b9cde11c..60b6b104 100644 --- a/lib/phoenix_test/assertions.ex +++ b/lib/phoenix_test/assertions.ex @@ -296,7 +296,7 @@ defmodule PhoenixTest.Assertions do |> append_found_other_matches(selector, other_matches) end - defp refute_found_error_msg(selector, opts, found) do + def refute_found_error_msg(selector, opts, found) do refute_count = Keyword.get(opts, :count, :any) at = Keyword.get(opts, :at, :any) text = Keyword.get(opts, :text, :no_text) diff --git a/lib/phoenix_test/case.ex b/lib/phoenix_test/case.ex index 0d711277..3848944a 100644 --- a/lib/phoenix_test/case.ex +++ b/lib/phoenix_test/case.ex @@ -1,10 +1,38 @@ defmodule PhoenixTest.Case do - @moduledoc false + @moduledoc """ + ExUnit case module to assist with browser based tests. + See `PhoenixTest.Playwright` for more information. + + ## Configuration + Set browser launch options via a `@moduletag` or `setup_all`:application + + ```ex + @moduletag playwright: [browser: :chromium, headless: false, slowMo: 1000] + ``` + + You can opt out of Playwright for selected tests via tags: + + ```ex + describe "part of feature without javascript" + @describetag playwright: false + + test "regular dead or live view without javascript" do + """ use ExUnit.CaseTemplate alias PhoenixTest.Case + using opts do + quote do + import PhoenixTest + + setup do + [phoenix_test: unquote(opts)] + end + end + end + @playwright_opts [ browser: :chromium, headless: true, @@ -27,8 +55,11 @@ defmodule PhoenixTest.Case do setup context do case context do - %{playwright: p, browser_id: browser_id} when p != false -> [conn: Case.Playwright.session(browser_id)] - _ -> [conn: Phoenix.ConnTest.build_conn()] + %{playwright: p} when p != false -> + [conn: Case.Playwright.start_session(context)] + + _ -> + [conn: Phoenix.ConnTest.build_conn()] end end @@ -44,13 +75,27 @@ defmodule PhoenixTest.Case do browser_id end - def session(browser_id) do - context_id = sync_post(guid: browser_id, method: "newContext").result.context.guid + def start_session(%{browser_id: browser_id} = context) do + params = browser_context_params(context) + context_id = sync_post(guid: browser_id, method: "newContext", params: params).result.context.guid + on_exit(fn -> post(guid: context_id, method: "close") end) + page_id = sync_post(guid: context_id, method: "newPage").result.page.guid [%{params: %{guid: "frame" <> _ = frame_id}}] = responses(page_id) - on_exit(fn -> post(guid: context_id, method: "close") end) PhoenixTest.Playwright.build(frame_id) end + + if Code.ensure_loaded?(Phoenix.Ecto.SQL.Sandbox) do + defp browser_context_params(%{repo: repo} = context) do + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(repo, shared: not context[:async]) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) + metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(repo, pid) + encoded = {:v1, metadata} |> :erlang.term_to_binary() |> Base.url_encode64() + %{userAgent: "BeamMetadata (#{encoded})"} + end + end + + defp browser_context_params(_), do: %{} end end diff --git a/lib/phoenix_test/playwright.ex b/lib/phoenix_test/playwright.ex index ae03aa34..bdda4abf 100644 --- a/lib/phoenix_test/playwright.ex +++ b/lib/phoenix_test/playwright.ex @@ -1,5 +1,73 @@ defmodule PhoenixTest.Playwright do - @moduledoc false + @moduledoc """ + > #### Warning {: .warning} + > + > This feature is experimental. + > If you don't need browser based tests, see `m:PhoenixTest#module-usage` on regular usage. + + Test driver to run tests in an actual (usually headless) browser via [Playwright](https://playwright.dev/). + + + ## Setup + + 1. Install [playwright](https://www.npmjs.com/package/playwright) + 2. Install playwright browsers: `npm exec --prefix assets playwright install --with-deps` + 3. Add to `config/test.exs`: `config :phoenix_test, playwright_cli: "assets/node_modules/playwright/cli.js"` + 4. Add to `test/test_helpers.exs`: `Application.put_env(:phoenix_test, :base_url, YourWeb.Endpoint.url())` + + + ## Usage + ```ex + defmodule MyFeatureTest do + use PhoenixTest.Case, + async: true, + parameterize: [%{playwright: [browser: :chromium]}, %{playwright: [browser: :firefox]}] + + test "heading", %{conn: conn} do + conn + |> visit("/") + |> assert_has("h1", text: "Heading") + end + end + end + ``` + + As shown above, you can use `m:ExUnit.Case#module-parameterized-tests` parameterized tests + to run tests concurrently in different browsers. + + + ## Known limitations and inconsistencies + + - `PhoenixTest.select/4` option `exact_option` is not supported + - Playwright driver is less strict than `Live` and `Static` drivers. It does not raise errors + - when visiting a page that returns a `404` status + - when interactive elements such as forms and buttons are missing essential attributes (`phx-click`, `phx-submit`, `action`) + - A few small bugs + + See tests tagged with [`@tag playwright: false`](https://github.com/search?q=repo%3Agermsvel%2Fphoenix_test%20%22%40tag%20playwright%3A%20false%22&type=code) + for details. + + + ## Configuration + + In `config/test.exs`: + + ```elixir + config :phoenix_test, + playwright_cli: "assets/node_modules/playwright/cli.js", + timeout_ms: 1000 + ``` + + ## Ecto SQL.Sandbox + + Pass the `repo` option to enable Ecto sandboxing. + This allows for concurrent browser tests (based on [this guide](https://hexdocs.pm/phoenix_ecto/main.html#concurrent-browser-tests)). + + ```ex + defmodule MyTest do + use PhoenixTest.Case, async: true, repo: MyApp.Repo + ``` + """ alias PhoenixTest.Assertions alias PhoenixTest.Element.Button @@ -13,13 +81,13 @@ defmodule PhoenixTest.Playwright do defstruct [:frame_id, :last_input_selector, within: :none] @endpoint Application.compile_env(:phoenix_test, :endpoint) - @default_timeout :timer.seconds(1) + @default_timeout_ms 1000 def build(frame_id) do %__MODULE__{frame_id: frame_id} end - def retry(fun, backoff_ms \\ [100, 250, 500, 1000]) + def retry(fun, backoff_ms \\ [100, 250, 500, timeout()]) def retry(fun, []), do: fun.() def retry(fun, [sleep_ms | backoff_ms]) do @@ -80,7 +148,7 @@ defmodule PhoenixTest.Playwright do selector = session |> maybe_within() - |> Selector.concat(Selector.css_or_locator(selector)) + |> Selector.concat(Selector.css(selector)) |> Selector.concat(Selector.text(opts[:text], opts)) |> Selector.concat(Selector.at(opts[:at])) @@ -130,8 +198,8 @@ defmodule PhoenixTest.Playwright do selector = session |> maybe_within() - |> Selector.concat(Selector.css_or_locator(css_selector)) - |> Selector.concat(Selector.text(text, exact: false)) + |> Selector.concat(Selector.css(css_selector)) + |> Selector.concat(Selector.text(text, exact: true)) session.frame_id |> Frame.click(selector, %{timeout: timeout()}) @@ -144,8 +212,8 @@ defmodule PhoenixTest.Playwright do selector = session |> maybe_within() - |> Selector.concat(Selector.css_or_locator(css_selector)) - |> Selector.concat(Selector.text(text, exact: false)) + |> Selector.concat(Selector.css(css_selector)) + |> Selector.concat(Selector.text(text, exact: true)) session.frame_id |> Frame.click(selector, %{timeout: timeout()}) @@ -202,7 +270,7 @@ defmodule PhoenixTest.Playwright do selector = session |> maybe_within() - |> Selector.concat(Selector.css_or_locator(input_selector)) + |> Selector.concat(Selector.css(input_selector)) |> Selector.and(Selector.label(label, opts)) selector @@ -276,7 +344,7 @@ defmodule PhoenixTest.Playwright do end defp timeout(opts \\ []) do - default = Application.get_env(:phoenix_test, :timeout, @default_timeout) + default = Application.get_env(:phoenix_test, :timeout_ms, @default_timeout_ms) Keyword.get(opts, :timeout, default) end end diff --git a/lib/phoenix_test/playwright/connection.ex b/lib/phoenix_test/playwright/connection.ex index c2fdead8..cd95f64f 100644 --- a/lib/phoenix_test/playwright/connection.ex +++ b/lib/phoenix_test/playwright/connection.ex @@ -1,9 +1,18 @@ defmodule PhoenixTest.Playwright.Connection do - @moduledoc false + @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 + @default_timeout_ms 1000 + @playwright_timeout_grace_period_ms 100 + defstruct [ :port, :init, @@ -12,33 +21,58 @@ defmodule PhoenixTest.Playwright.Connection do pending_response: %{} ] + @name __MODULE__ + def start_link(config) do - GenServer.start_link(__MODULE__, config, name: __MODULE__, timeout: 1000) + GenServer.start_link(__MODULE__, config, name: @name, timeout: timeout()) end - def ensure_started(name \\ __MODULE__, config) do - case Process.whereis(name) do + @doc """ + Lazy launch. Only start the playwright server if actually needed by a test. + """ + def ensure_started(config) do + case Process.whereis(@name) do nil -> start_link(config) pid -> {:ok, pid} end end - def launch_browser(name \\ __MODULE__, type, opts) do - type_id = GenServer.call(name, {:browser_type_id, type}) + @doc """ + Launch a browser and return its `guid`. + """ + def launch_browser(type, opts) do + type_id = GenServer.call(@name, {:browser_type_id, type}) resp = sync_post(guid: type_id, method: "launch", params: Map.new(opts)) resp.result.browser.guid end - def post(name \\ __MODULE__, msg) do - GenServer.cast(name, {:post, msg}) + @doc """ + Fire and forget. + """ + def post(msg) do + GenServer.cast(@name, {:post, msg}) end - def sync_post(name \\ __MODULE__, msg) do - GenServer.call(name, {:sync_post, msg}) + @doc """ + Post a message and await the response. + We wait for an additional grace period after the timeout that we pass to playwright. + + We use double the default timeout if there is no message timeout, since some + playwright operations use a backoff internally ([100, 250, 500, 1000]). + """ + def sync_post(msg) do + timeout = msg[:params][:timeout] || 2 * timeout() + timeout_with_grace_period = timeout + @playwright_timeout_grace_period_ms + GenServer.call(@name, {:sync_post, msg}, timeout_with_grace_period) end - def responses(name \\ __MODULE__, guid) do - GenServer.call(name, {:responses, guid}) + @doc """ + Get all past responses for a playwright `guid` (e.g. a `Frame`). + The internal map used to track these responses is never cleaned, it will keep on growing. + Since we're dealing with (short-lived) tests, that should be fine. + """ + def responses(guid) do + GenServer.call(@name, {:responses, guid}) end @impl GenServer @@ -108,4 +142,8 @@ defmodule PhoenixTest.Playwright.Connection do defp handle_recv(_msg, state), do: state defp browser_type_id(init, type), do: Map.fetch!(init, type).guid + + defp timeout do + Application.get_env(:phoenix_test, :timeout_ms, @default_timeout_ms) + end end diff --git a/lib/phoenix_test/playwright/frame.ex b/lib/phoenix_test/playwright/frame.ex index e985ded1..13a52d31 100644 --- a/lib/phoenix_test/playwright/frame.ex +++ b/lib/phoenix_test/playwright/frame.ex @@ -1,5 +1,13 @@ defmodule PhoenixTest.Playwright.Frame do - @moduledoc false + @moduledoc """ + Interact with a Playwright `Frame` (usually the "main" frame of a browser page). + + There is no official documentation, since this is considered Playwright internal. + + References: + - https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/frame.ts + """ + import PhoenixTest.Playwright.Connection, only: [sync_post: 1] def goto(frame_id, url) do diff --git a/lib/phoenix_test/playwright/message.ex b/lib/phoenix_test/playwright/message.ex index 84b344d6..fd6e3ee4 100644 --- a/lib/phoenix_test/playwright/message.ex +++ b/lib/phoenix_test/playwright/message.ex @@ -1,5 +1,9 @@ defmodule PhoenixTest.Playwright.Message do - @moduledoc false + @moduledoc """ + Prase playwright messages. + One message received by the `Port` might contain multiple Playwright messages. + Also, it might contain only part of a `Playwright` message, to be continued in the next `Port` message. + """ def parse(<>, 0, "", accumulated) do %{ diff --git a/lib/phoenix_test/playwright/port.ex b/lib/phoenix_test/playwright/port.ex index 053bcf23..1ee8040a 100644 --- a/lib/phoenix_test/playwright/port.ex +++ b/lib/phoenix_test/playwright/port.ex @@ -1,5 +1,7 @@ defmodule PhoenixTest.Playwright.Port do - @moduledoc false + @moduledoc """ + Start a Playwright node.js server and communicate with it via a `Port`. + """ alias PhoenixTest.Playwright.Message @@ -35,7 +37,8 @@ defmodule PhoenixTest.Playwright.Port do end defp default_cli do - Path.join(:code.priv_dir(:phoenix_test), "static/driver.js") + fallback = Path.join(:code.priv_dir(:phoenix_test), "static/driver.js") + Application.get_env(:phoenix_test, :playwright_cli, fallback) end defp deserialize(json) do diff --git a/lib/phoenix_test/playwright/selector.ex b/lib/phoenix_test/playwright/selector.ex index 1c97590e..3449dbe7 100644 --- a/lib/phoenix_test/playwright/selector.ex +++ b/lib/phoenix_test/playwright/selector.ex @@ -1,5 +1,19 @@ defmodule PhoenixTest.Playwright.Selector do - @moduledoc false + @moduledoc """ + Playright supports different types of locators: CSS, XPath, internal. + + They can mixed and matched by chaining the together. + + Also, you can register [custom selector engines](https://playwright.dev/docs/extensibility#custom-selector-engines) + that run right in the browser (Javascript). + + There is no official documentation, since this is considered Playwright internal. + + References: + - https://playwright.dev/docs/other-locators + - https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/locator.ts + - https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/utils/isomorphic/locatorUtils.ts + """ def concat(left, :none), do: left def concat(left, right), do: "#{left} >> #{right}" @@ -17,20 +31,10 @@ defmodule PhoenixTest.Playwright.Selector do def at(nil), do: :none def at(at), do: "nth=#{at}" - def css_or_locator(nil), do: :none - def css_or_locator([]), do: :none - def css_or_locator(selector) when is_binary(selector), do: css_or_locator([selector]) - def css_or_locator(selectors) when is_list(selectors), do: "css=#{Enum.join(selectors, ",")}" - - def css_or_locator(%PhoenixTest.Locators.Input{} = input) do - attrs = - input - |> Map.take(~w(type value)a) - |> Enum.reject(fn {_, v} -> is_nil(v) end) - |> Enum.map_join("", fn {k, v} -> "[#{k}='#{v}']" end) - - input.label |> label(exact: true) |> _and(css_or_locator(attrs)) - end + def css(nil), do: :none + def css([]), do: :none + def css(selector) when is_binary(selector), do: css([selector]) + def css(selectors) when is_list(selectors), do: "css=#{Enum.join(selectors, ",")}" defp exact_suffix(opts) when is_list(opts), do: opts |> Keyword.get(:exact, false) |> exact_suffix() defp exact_suffix(true), do: "s" diff --git a/test/phoenix_test/assertions_test.exs b/test/phoenix_test/assertions_test.exs index c4d4ea67..9aa7bd62 100644 --- a/test/phoenix_test/assertions_test.exs +++ b/test/phoenix_test/assertions_test.exs @@ -1,7 +1,6 @@ defmodule PhoenixTest.AssertionsTest do use PhoenixTest.Case, async: true, parameterize: [%{playwright: false}, %{playwright: true}] - import PhoenixTest import PhoenixTest.Locators import PhoenixTest.TestHelpers @@ -43,6 +42,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("li") end + @tag playwright: false, reason: {:not_implemented, :locators} test "takes in input helper in assertion", %{conn: conn} do conn |> visit("/page/index") diff --git a/test/phoenix_test/live_test.exs b/test/phoenix_test/live_test.exs index 75de2bd8..d9b7f93b 100644 --- a/test/phoenix_test/live_test.exs +++ b/test/phoenix_test/live_test.exs @@ -1,8 +1,6 @@ defmodule PhoenixTest.LiveTest do use PhoenixTest.Case, async: true, parameterize: [%{playwright: false}, %{playwright: true}] - import PhoenixTest - alias PhoenixTest.Driver describe "assert_has/2 title" do @@ -205,9 +203,7 @@ defmodule PhoenixTest.LiveTest do |> refute_has("#form-data", text: "email: some@example.com") end - test "submits owner form if button isn't nested inside form (including button data)", %{ - conn: conn - } do + test "submits owner form if button isn't nested inside form (including button data)", %{conn: conn} do conn |> visit("/live/index") |> within("#owner-form", fn session -> @@ -230,7 +226,7 @@ defmodule PhoenixTest.LiveTest do conn |> visit("/live/index") |> within("#redirect-form-to-static", &fill_in(&1, "Name", with: "Aragorn")) - |> click_button("#redirect-form-to-static-submit", "Save Redirect to Static") + |> click_button("#redirect-form-to-static-submit", "Save Redirect to Static Form") |> assert_has("h1", text: "Main page") end @@ -692,9 +688,7 @@ defmodule PhoenixTest.LiveTest do |> assert_has("#form-data", text: "book-or-movie: book") end - test "can specify input selector when multiple options have same label in same form", %{ - conn: conn - } do + test "can specify input selector when multiple options have same label in same form", %{conn: conn} do conn |> visit("/live/index") |> within("#same-labels", fn session -> @@ -748,8 +742,7 @@ defmodule PhoenixTest.LiveTest do end @tag playwright: false, reason: :known_inconsistency - test "upload (without other form actions) does not work with submit (matches browser behavior)", - %{conn: conn} do + test "upload (without other form actions) does not work with submit (matches browser behavior)", %{conn: conn} do session = conn |> visit("/live/index") diff --git a/test/phoenix_test/playwright_test.exs b/test/phoenix_test/playwright_test.exs index 42b6cd6b..312c9a81 100644 --- a/test/phoenix_test/playwright_test.exs +++ b/test/phoenix_test/playwright_test.exs @@ -3,8 +3,6 @@ defmodule PhoenixTest.PlaywrightTest do async: true, parameterize: Enum.map(~w(chromium firefox)a, &%{playwright: [browser: &1]}) - import PhoenixTest - describe "render_page_title/1" do unless System.version() in ~w(1.15.0 1.16.0 1.17.0) do test "runs in multiple browsers via ExUnit `parameterize`", %{conn: conn} do diff --git a/test/phoenix_test/static_test.exs b/test/phoenix_test/static_test.exs index a20a9dbb..3e4356a8 100644 --- a/test/phoenix_test/static_test.exs +++ b/test/phoenix_test/static_test.exs @@ -1,7 +1,6 @@ defmodule PhoenixTest.StaticTest do use PhoenixTest.Case, async: true, parameterize: [%{playwright: false}, %{playwright: true}] - import PhoenixTest import PhoenixTest.TestHelpers describe "render_page_title/1" do @@ -661,9 +660,9 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "book-or-movie: book") end - test "can specify input selector when multiple options have same label in same form", %{ - conn: conn - } do + # Playwright: Can't find with `exact: true` + @tag playwright: false, reason: :bug + test "can specify input selector when multiple options have same label in same form", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> @@ -720,7 +719,7 @@ defmodule PhoenixTest.StaticTest do # Playwright: 'Enter' key on file input oppens file picker dialog @tag playwright: false, reason: :bug - test("can specify input selector when multiple inputs have same label", %{conn: conn}) do + test "can specify input selector when multiple inputs have same label", %{conn: conn} do conn |> visit("/page/index") |> within("#same-labels", fn session -> From f16ab9d71f9c6ce21746d5d2ae12f2b9dc8fc563 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Sat, 2 Nov 2024 08:47:18 +0100 Subject: [PATCH 4/6] Test Ecto sandbox --- .github/workflows/ci.yml | 18 +++++++++- config/test.exs | 14 +++++++- lib/phoenix_test/case.ex | 50 +++++++++++++------------- lib/phoenix_test/playwright.ex | 15 ++++---- lib/phoenix_test/playwright/message.ex | 6 ++-- lib/phoenix_test/playwright/port.ex | 20 +++++++---- mix.exs | 9 ++++- mix.lock | 8 ++++- priv/repo/migrations/.formatter.exs | 4 +++ test/support/endpoint.ex | 2 ++ test/support/repo.ex | 5 +++ test/test_helper.exs | 3 ++ 12 files changed, 109 insertions(+), 45 deletions(-) create mode 100644 priv/repo/migrations/.formatter.exs create mode 100644 test/support/repo.ex diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d353598..d9cdd985 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,19 @@ permissions: jobs: test: + services: + postgres: + image: postgres + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: phoenix_test_test + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 runs-on: ubuntu-latest name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: @@ -75,8 +88,11 @@ jobs: - name: Build assets for browser tests run: mix do assets.setup, assets.build + - name: Setup database + run: mix ecto.setup + - name: Install playwright browsers run: npm exec --prefix priv/static/assets playwright install --with-deps - name: Run tests - run: "mix test || if [[ $? = 2 ]]; then mix test --failed; else false; fi" + run: "mix test || if [[ $? = 2 ]]; then mix test --failed; else false; fi" diff --git a/config/test.exs b/config/test.exs index 68cbde9f..05811abd 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,6 +1,10 @@ import Config -config :phoenix_test, :endpoint, PhoenixTest.Endpoint +config :phoenix_test, + endpoint: PhoenixTest.Endpoint, + ecto_repos: [PhoenixTest.Repo], + otp_app: :phoenix_test, + playwright_cli: "priv/static/assets/node_modules/playwright/cli.js" config :logger, level: :warning @@ -20,3 +24,11 @@ config :esbuild, cd: Path.expand("../test/assets", __DIR__), env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} ] + +config :phoenix_test, PhoenixTest.Repo, + username: "postgres", + password: "postgres", + hostname: "localhost", + database: "phoenix_test_test#{System.get_env("MIX_TEST_PARTITION")}", + pool: Ecto.Adapters.SQL.Sandbox, + pool_size: 10 diff --git a/lib/phoenix_test/case.ex b/lib/phoenix_test/case.ex index 3848944a..7c7eb0a5 100644 --- a/lib/phoenix_test/case.ex +++ b/lib/phoenix_test/case.ex @@ -2,21 +2,6 @@ defmodule PhoenixTest.Case do @moduledoc """ ExUnit case module to assist with browser based tests. See `PhoenixTest.Playwright` for more information. - - ## Configuration - Set browser launch options via a `@moduletag` or `setup_all`:application - - ```ex - @moduletag playwright: [browser: :chromium, headless: false, slowMo: 1000] - ``` - - You can opt out of Playwright for selected tests via tags: - - ```ex - describe "part of feature without javascript" - @describetag playwright: false - - test "regular dead or live view without javascript" do """ use ExUnit.CaseTemplate @@ -67,6 +52,9 @@ defmodule PhoenixTest.Case do @moduledoc false import PhoenixTest.Playwright.Connection + @includes_ecto Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) && + Code.ensure_loaded?(Phoenix.Ecto.SQL.Sandbox) + def launch_browser(opts) do opts = Map.new(opts) ensure_started(opts) @@ -76,7 +64,8 @@ defmodule PhoenixTest.Case do end def start_session(%{browser_id: browser_id} = context) do - params = browser_context_params(context) + user_agent = checkout_ecto_repos(context[:async]) + params = if user_agent, do: %{userAgent: user_agent}, else: %{} context_id = sync_post(guid: browser_id, method: "newContext", params: params).result.context.guid on_exit(fn -> post(guid: context_id, method: "close") end) @@ -86,16 +75,27 @@ defmodule PhoenixTest.Case do PhoenixTest.Playwright.build(frame_id) end - if Code.ensure_loaded?(Phoenix.Ecto.SQL.Sandbox) do - defp browser_context_params(%{repo: repo} = context) do - pid = Ecto.Adapters.SQL.Sandbox.start_owner!(repo, shared: not context[:async]) - on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) - metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(repo, pid) - encoded = {:v1, metadata} |> :erlang.term_to_binary() |> Base.url_encode64() - %{userAgent: "BeamMetadata (#{encoded})"} + if @includes_ecto do + def checkout_ecto_repos(async?) do + otp_app = Application.fetch_env!(:phoenix_test, :otp_app) + repos = Application.fetch_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 - end - defp browser_context_params(_), do: %{} + defp checkout_ecto_repo(repo, async?) do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(repo) + unless async?, do: Ecto.Adapters.SQL.Sandbox.mode(repo, {:shared, self()}) + + repo + end + else + def checkout_ecto_repos(_) do + nil + end + end end end diff --git a/lib/phoenix_test/playwright.ex b/lib/phoenix_test/playwright.ex index bdda4abf..f400bc59 100644 --- a/lib/phoenix_test/playwright.ex +++ b/lib/phoenix_test/playwright.ex @@ -5,15 +5,15 @@ defmodule PhoenixTest.Playwright do > This feature is experimental. > If you don't need browser based tests, see `m:PhoenixTest#module-usage` on regular usage. - Test driver to run tests in an actual (usually headless) browser via [Playwright](https://playwright.dev/). + Run tests tests in a one or more browsers via [Playwright](https://playwright.dev/). ## Setup - 1. Install [playwright](https://www.npmjs.com/package/playwright) + 1. Install or vendor [playwright](https://www.npmjs.com/package/playwright) using your existing JS pipeline 2. Install playwright browsers: `npm exec --prefix assets playwright install --with-deps` - 3. Add to `config/test.exs`: `config :phoenix_test, playwright_cli: "assets/node_modules/playwright/cli.js"` - 4. Add to `test/test_helpers.exs`: `Application.put_env(:phoenix_test, :base_url, YourWeb.Endpoint.url())` + 3. Add to `config/test.exs`: `config :phoenix_test, otp_app: :your_app, playwright_cli: "assets/node_modules/playwright/cli.js"` + 4. Add to `test/test_helpers.exs`: `Application.put_env(:phoenix_test, :base_url, YourAppWeb.Endpoint.url())` ## Usage @@ -60,12 +60,13 @@ defmodule PhoenixTest.Playwright do ## Ecto SQL.Sandbox - Pass the `repo` option to enable Ecto sandboxing. - This allows for concurrent browser tests (based on [this guide](https://hexdocs.pm/phoenix_ecto/main.html#concurrent-browser-tests)). + `PhoenixTest.Case` automatically takes care of this. + It passes a user agent referencing your Ecto repos. + This allows for [concurrent browser tests](https://hexdocs.pm/phoenix_ecto/main.html#concurrent-browser-tests). ```ex defmodule MyTest do - use PhoenixTest.Case, async: true, repo: MyApp.Repo + use PhoenixTest.Case, async: true ``` """ diff --git a/lib/phoenix_test/playwright/message.ex b/lib/phoenix_test/playwright/message.ex index fd6e3ee4..b1705556 100644 --- a/lib/phoenix_test/playwright/message.ex +++ b/lib/phoenix_test/playwright/message.ex @@ -1,8 +1,8 @@ defmodule PhoenixTest.Playwright.Message do @moduledoc """ - Prase playwright messages. - One message received by the `Port` might contain multiple Playwright messages. - Also, it might contain only part of a `Playwright` message, to be continued in the next `Port` message. + Parse playwright messages. + A single `Port` message can contain multiple Playwright messages and/or a fraction of a message. + Such a message fraction is stored in `bufffer` and continued in the next `Port` message. """ def parse(<>, 0, "", accumulated) do diff --git a/lib/phoenix_test/playwright/port.ex b/lib/phoenix_test/playwright/port.ex index 1ee8040a..e6193277 100644 --- a/lib/phoenix_test/playwright/port.ex +++ b/lib/phoenix_test/playwright/port.ex @@ -12,7 +12,20 @@ defmodule PhoenixTest.Playwright.Port do ] def open(config) do - cli = Map.get(config, :driver_path, default_cli()) + cli = Map.get_lazy(config, :driver_path, fn -> Application.fetch_env!(:phoenix_test, :playwright_cli) end) + + unless File.exists?(cli) do + msg = """ + Could not find playwright CLI at #{cli}. + + To resolve this please + 1. Install playwright, e.g. `npm i playwright` + 2. Configure the path correctly, e.g. in `config/text.exs`: `config :phoenix_test, playwright_cli: "assets/node_modules/playwright/cli.js"` + """ + + raise ArgumentError, msg + end + cmd = "run-driver" port = Port.open({:spawn, "#{cli} #{cmd}"}, [:binary]) @@ -36,11 +49,6 @@ defmodule PhoenixTest.Playwright.Port do {state, msgs} end - defp default_cli do - fallback = Path.join(:code.priv_dir(:phoenix_test), "static/driver.js") - Application.get_env(:phoenix_test, :playwright_cli, fallback) - end - defp deserialize(json) do case Jason.decode(json) do {:ok, data} -> atom_keys(data) diff --git a/mix.exs b/mix.exs index af2aad37..39d17173 100644 --- a/mix.exs +++ b/mix.exs @@ -50,6 +50,10 @@ defmodule PhoenixTest.MixProject do {:mime, ">= 1.0.0", optional: true}, {:phoenix, "~> 1.7.10"}, {:phoenix_live_view, "~> 0.20.1"}, + {:ecto, "~> 3.12", optional: true}, + {:ecto_sql, "~> 3.12", optional: true}, + {:phoenix_ecto, "~> 4.6", optional: true}, + {:postgrex, "~> 0.19.2", option: true}, {:plug_cowboy, "~> 2.7", only: :test, runtime: false}, {:styler, "~> 0.11", only: [:dev, :test], runtime: false} ] @@ -77,7 +81,10 @@ defmodule PhoenixTest.MixProject do defp aliases do [ - setup: ["deps.get", "assets.setup", "assets.build"], + setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], + "ecto.setup": ["ecto.create", "ecto.migrate"], + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], "assets.setup": ["esbuild.install --if-missing"], "assets.build": ["esbuild default"] ] diff --git a/mix.lock b/mix.lock index 0685833d..87a35c8b 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,11 @@ "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ecto": {:hex, :ecto, "3.12.4", "267c94d9f2969e6acc4dd5e3e3af5b05cdae89a4d549925f3008b2b7eb0b93c3", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ef04e4101688a67d061e1b10d7bc1fbf00d1d13c17eef08b71d070ff9188f747"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, "ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"}, "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, @@ -17,13 +21,15 @@ "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, - "phoenix_html": {:hex, :phoenix_html, "4.0.0", "4857ec2edaccd0934a923c2b0ba526c44a173c86b847e8db725172e9e51d11d6", [:mix], [], "hexpm", "cee794a052f243291d92fa3ccabcb4c29bb8d236f655fb03bcbdc3a8214b8d13"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, + "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.3", "8b6406bc0a451f295407d7acff7f234a6314be5bbe0b3f90ed82b07f50049878", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8e4385e05618b424779f894ed2df97d3c7518b7285fcd11979077ae6226466b"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.15.2", "94cf1fa375526f30ff8770837cb804798e0045fd97185f0bb9e5fcd858c792a3", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02731fa0c2dcb03d8d21a1d941bdbbe99c2946c0db098eee31008e04c6283615"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, + "postgrex": {:hex, :postgrex, "0.19.2", "34d6884a332c7bf1e367fc8b9a849d23b43f7da5c6e263def92784d03f9da468", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "618988886ab7ae8561ebed9a3c7469034bf6a88b8995785a3378746a4b9835ec"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "styler": {:hex, :styler, "0.11.9", "2595393b94e660cd6e8b582876337cc50ff047d184ccbed42fdad2bfd5d78af5", [:mix], [], "hexpm", "8b7806ba1fdc94d0a75127c56875f91db89b75117fcc67572661010c13e1f259"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, diff --git a/priv/repo/migrations/.formatter.exs b/priv/repo/migrations/.formatter.exs new file mode 100644 index 00000000..49f9151e --- /dev/null +++ b/priv/repo/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto_sql], + inputs: ["*.exs"] +] diff --git a/test/support/endpoint.ex b/test/support/endpoint.ex index a6d3f5bc..af7eb8e4 100644 --- a/test/support/endpoint.ex +++ b/test/support/endpoint.ex @@ -12,4 +12,6 @@ defmodule PhoenixTest.Endpoint do plug Plug.MethodOverride plug PhoenixTest.Router + + plug Phoenix.Ecto.SQL.Sandbox end diff --git a/test/support/repo.ex b/test/support/repo.ex new file mode 100644 index 00000000..dbc2c052 --- /dev/null +++ b/test/support/repo.ex @@ -0,0 +1,5 @@ +defmodule PhoenixTest.Repo do + use Ecto.Repo, + otp_app: :phoenix_test, + adapter: Ecto.Adapters.Postgres +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 5703ed83..c3442c95 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,6 +1,9 @@ ExUnit.start() {:ok, _} = Supervisor.start_link([{Phoenix.PubSub, name: PhoenixTest.PubSub}], strategy: :one_for_one) +{:ok, _} = PhoenixTest.Repo.start_link() {:ok, _} = PhoenixTest.Endpoint.start_link() Application.put_env(:phoenix_test, :base_url, PhoenixTest.Endpoint.url()) + +Ecto.Adapters.SQL.Sandbox.mode(PhoenixTest.Repo, :manual) From e16c76c60fdb8f6d2e7da89a25ff5114915c514e Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Sun, 24 Nov 2024 19:01:29 +0100 Subject: [PATCH 5/6] Add tracing --- config/test.exs | 6 +- lib/phoenix_test/case.ex | 47 +++-- lib/phoenix_test/playwright.ex | 42 +++-- lib/phoenix_test/playwright/browser.ex | 20 +++ .../playwright/browser_context.ex | 48 +++++ lib/phoenix_test/playwright/connection.ex | 170 +++++++++++++----- lib/phoenix_test/playwright/frame.ex | 34 ++-- lib/phoenix_test/playwright/port.ex | 10 +- priv/static/assets/package-lock.json | 18 +- 9 files changed, 289 insertions(+), 106 deletions(-) create mode 100644 lib/phoenix_test/playwright/browser.ex create mode 100644 lib/phoenix_test/playwright/browser_context.ex diff --git a/config/test.exs b/config/test.exs index 05811abd..50c5387a 100644 --- a/config/test.exs +++ b/config/test.exs @@ -4,7 +4,11 @@ config :phoenix_test, endpoint: PhoenixTest.Endpoint, ecto_repos: [PhoenixTest.Repo], otp_app: :phoenix_test, - playwright_cli: "priv/static/assets/node_modules/playwright/cli.js" + playwright: [ + browser: [browser: :chromium, headless: System.get_env("PLAYWRIGHT_HEADLESS", "t") in ~w(t true)], + cli: "priv/static/assets/node_modules/playwright/cli.js", + trace: System.get_env("PLAYWRIGHT_TRACE", "false") in ~w(t true) + ] config :logger, level: :warning diff --git a/lib/phoenix_test/case.ex b/lib/phoenix_test/case.ex index 7c7eb0a5..e68f38dd 100644 --- a/lib/phoenix_test/case.ex +++ b/lib/phoenix_test/case.ex @@ -25,13 +25,15 @@ defmodule PhoenixTest.Case do ] setup_all context do + trace = Application.fetch_env!(:phoenix_test, :playwright)[:trace] + case context do %{playwright: true} -> - [browser_id: Case.Playwright.launch_browser(@playwright_opts)] + [browser_id: Case.Playwright.launch_browser(@playwright_opts), trace: trace] %{playwright: opts} when is_list(opts) -> opts = Keyword.merge(@playwright_opts, opts) - [browser_id: Case.Playwright.launch_browser(opts)] + [browser_id: Case.Playwright.launch_browser(opts), trace: trace] _ -> :ok @@ -41,7 +43,7 @@ defmodule PhoenixTest.Case do setup context do case context do %{playwright: p} when p != false -> - [conn: Case.Playwright.start_session(context)] + [conn: Case.Playwright.new_session(context)] _ -> [conn: Phoenix.ConnTest.build_conn()] @@ -52,27 +54,44 @@ defmodule PhoenixTest.Case do @moduledoc false import PhoenixTest.Playwright.Connection + alias PhoenixTest.Playwright.Browser + alias PhoenixTest.Playwright.BrowserContext + @includes_ecto Code.ensure_loaded?(Ecto.Adapters.SQL.Sandbox) && Code.ensure_loaded?(Phoenix.Ecto.SQL.Sandbox) def launch_browser(opts) do - opts = Map.new(opts) - ensure_started(opts) - browser_id = launch_browser(opts.browser, opts) - on_exit(fn -> sync_post(guid: browser_id, method: "close") end) + ensure_started() + browser = Keyword.fetch!(opts, :browser) + browser_id = launch_browser(browser, opts) + on_exit(fn -> post(guid: browser_id, method: "close") end) browser_id end - def start_session(%{browser_id: browser_id} = context) do - user_agent = checkout_ecto_repos(context[:async]) - params = if user_agent, do: %{userAgent: user_agent}, else: %{} - context_id = sync_post(guid: browser_id, method: "newContext", params: params).result.context.guid + def new_session(%{browser_id: browser_id} = context) do + context_id = Browser.new_context(browser_id) + subscribe(context_id) + + page_id = BrowserContext.new_page(context_id) + post(%{method: :updateSubscription, guid: page_id, params: %{event: "console", enabled: true}}) + frame_id = initializer(page_id).mainFrame.guid on_exit(fn -> post(guid: context_id, method: "close") end) - page_id = sync_post(guid: context_id, method: "newPage").result.page.guid - [%{params: %{guid: "frame" <> _ = frame_id}}] = responses(page_id) + if context[:trace] do + BrowserContext.start_tracing(context_id) + + dir = :phoenix_test |> Application.fetch_env!(:playwright) |> Keyword.fetch!(:trace_dir) + File.mkdir_p!(dir) + + "Elixir." <> case = to_string(context.case) + session_id = System.unique_integer([:positive, :monotonic]) + file = String.replace("#{case}.#{context.test}_#{session_id}.zip", ~r/[^a-zA-Z0-9 \.]/, "_") + path = Path.join(dir, file) + + on_exit(fn -> BrowserContext.stop_tracing(context_id, path) end) + end - PhoenixTest.Playwright.build(frame_id) + PhoenixTest.Playwright.build(page_id, frame_id) end if @includes_ecto do diff --git a/lib/phoenix_test/playwright.ex b/lib/phoenix_test/playwright.ex index f400bc59..dd5fc7dd 100644 --- a/lib/phoenix_test/playwright.ex +++ b/lib/phoenix_test/playwright.ex @@ -12,7 +12,7 @@ defmodule PhoenixTest.Playwright do 1. Install or vendor [playwright](https://www.npmjs.com/package/playwright) using your existing JS pipeline 2. Install playwright browsers: `npm exec --prefix assets playwright install --with-deps` - 3. Add to `config/test.exs`: `config :phoenix_test, otp_app: :your_app, playwright_cli: "assets/node_modules/playwright/cli.js"` + 3. Add to `config/test.exs`: `config :phoenix_test, otp_app: :your_app, playwright: [cli: "assets/node_modules/playwright/cli.js"]` 4. Add to `test/test_helpers.exs`: `Application.put_env(:phoenix_test, :base_url, YourAppWeb.Endpoint.url())` @@ -54,7 +54,12 @@ defmodule PhoenixTest.Playwright do ```elixir config :phoenix_test, - playwright_cli: "assets/node_modules/playwright/cli.js", + playwright: [ + cli: "assets/node_modules/playwright/cli.js", + browser: [browser: :chromium, headless: System.get_env("PLAYWRIGHT_HEADLESS", "t") in ~w(t true)], + trace: System.get_env("PLAYWRIGHT_TRACE", "false") in ~w(t true), + trace_dir: "tmp" + ], timeout_ms: 1000 ``` @@ -79,13 +84,15 @@ defmodule PhoenixTest.Playwright do alias PhoenixTest.Playwright.Selector alias PhoenixTest.Query - defstruct [:frame_id, :last_input_selector, within: :none] + require Logger + + defstruct [:page_id, :frame_id, :last_input_selector, within: :none] @endpoint Application.compile_env(:phoenix_test, :endpoint) @default_timeout_ms 1000 - def build(frame_id) do - %__MODULE__{frame_id: frame_id} + def build(page_id, frame_id) do + %__MODULE__{page_id: page_id, frame_id: frame_id} end def retry(fun, backoff_ms \\ [100, 250, 500, timeout()]) @@ -100,8 +107,14 @@ defmodule PhoenixTest.Playwright do end def visit(session, path) do - base_url = Application.fetch_env!(:phoenix_test, :base_url) - Frame.goto(session.frame_id, base_url <> path) + url = + case path do + "http://" <> _ -> path + "https://" <> _ -> path + _ -> Application.fetch_env!(:phoenix_test, :base_url) <> path + end + + Frame.goto(session.frame_id, url) session end @@ -203,7 +216,7 @@ defmodule PhoenixTest.Playwright do |> Selector.concat(Selector.text(text, exact: true)) session.frame_id - |> Frame.click(selector, %{timeout: timeout()}) + |> Frame.click(selector) |> handle_response(fn -> Link.find!(render_html(session), css_selector, text) end) session @@ -217,7 +230,7 @@ defmodule PhoenixTest.Playwright do |> Selector.concat(Selector.text(text, exact: true)) session.frame_id - |> Frame.click(selector, %{timeout: timeout()}) + |> Frame.click(selector) |> handle_response(fn -> Button.find!(render_html(session), css_selector, text) end) session @@ -237,7 +250,6 @@ defmodule PhoenixTest.Playwright do end def select(session, input_selector, option_labels, opts) do - # TODO Support exact_option if opts[:exact_option] != true, do: raise("exact_option not implemented") {label, opts} = Keyword.pop!(opts, :from) @@ -290,15 +302,17 @@ defmodule PhoenixTest.Playwright do defp handle_response(result, error_fun) do case result do - {:error, %{name: "TimeoutError"}} -> + {:error, %{error: %{error: %{name: "TimeoutError"}}} = error} -> + Logger.error(error) error_fun.() raise ExUnit.AssertionError, message: "Could not find element." - {:error, %{name: "Error", message: "Error: strict mode violation" <> _}} -> + {:error, %{error: %{error: %{name: "Error", message: "Error: strict mode violation" <> _}}} = error} -> + Logger.error(error) error_fun.() raise ExUnit.AssertionError, message: "Found more than one element." - {:error, %{name: "Error", message: "Clicking the checkbox did not change its state"}} -> + {:error, %{error: %{error: %{name: "Error", message: "Clicking the checkbox did not change its state"}}}} -> :ok {:ok, result} -> @@ -335,7 +349,7 @@ defmodule PhoenixTest.Playwright do def current_path(session) do resp = session.frame_id - |> Connection.responses() + |> Connection.received() |> Enum.find(&match?(%{method: "navigated", params: %{url: _}}, &1)) if resp == nil, do: raise(ArgumentError, "Could not find current path.") diff --git a/lib/phoenix_test/playwright/browser.ex b/lib/phoenix_test/playwright/browser.ex new file mode 100644 index 00000000..f1bf10bf --- /dev/null +++ b/lib/phoenix_test/playwright/browser.ex @@ -0,0 +1,20 @@ +defmodule PhoenixTest.Playwright.Browser do + @moduledoc """ + Interact with a Playwright `Browser`. + + There is no official documentation, since this is considered Playwright internal. + + References: + - https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/browser.ts + """ + + import PhoenixTest.Playwright.Connection, only: [post: 1] + + @doc """ + Start a new browser context and return its `guid`. + """ + def new_context(browser_id) do + resp = post(guid: browser_id, method: "newContext") + resp.result.context.guid + end +end diff --git a/lib/phoenix_test/playwright/browser_context.ex b/lib/phoenix_test/playwright/browser_context.ex new file mode 100644 index 00000000..f79a8bb6 --- /dev/null +++ b/lib/phoenix_test/playwright/browser_context.ex @@ -0,0 +1,48 @@ +defmodule PhoenixTest.Playwright.BrowserContext do + @moduledoc """ + Interact with a Playwright `BrowserContext`. + + There is no official documentation, since this is considered Playwright internal. + + References: + - https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/browserContext.ts + """ + + import PhoenixTest.Playwright.Connection, only: [post: 1, initializer: 1] + + @doc """ + Open a new browser page and return its `guid`. + """ + def new_page(context_id) do + resp = post(guid: context_id, method: "newPage") + resp.result.page.guid + end + + @doc """ + Start tracing. The results can be retrieved via `stop_tracing/2`. + """ + def start_tracing(context_id, opts \\ []) do + opts = Keyword.validate!(opts, screenshots: true, snapshots: true, sources: true) + tracing_id = initializer(context_id).tracing.guid + post(method: :tracingStart, guid: tracing_id, params: Map.new(opts)) + post(method: :tracingStartChunk, guid: tracing_id) + :ok + end + + @doc """ + Stop tracing and write zip file to specified output path. + + Trace can be viewed via either + - `npx playwright show-trace trace.zip` + - https://trace.playwright.dev + """ + def stop_tracing(context_id, output_path) do + tracing_id = initializer(context_id).tracing.guid + resp = post(method: :tracingStopChunk, guid: tracing_id, params: %{mode: "archive"}) + zip_id = resp.result.artifact.guid + zip_path = initializer(zip_id).absolutePath + File.cp!(zip_path, output_path) + post(method: :tracingStop, guid: tracing_id) + :ok + end +end diff --git a/lib/phoenix_test/playwright/connection.ex b/lib/phoenix_test/playwright/connection.ex index cd95f64f..006a1cdc 100644 --- a/lib/phoenix_test/playwright/connection.ex +++ b/lib/phoenix_test/playwright/connection.ex @@ -10,106 +10,123 @@ defmodule PhoenixTest.Playwright.Connection do alias PhoenixTest.Playwright.Port, as: PlaywrightPort + require Logger + @default_timeout_ms 1000 @playwright_timeout_grace_period_ms 100 defstruct [ :port, - :init, - responses: %{}, - pending_init: [], - pending_response: %{} + status: :pending, + awaiting_started: [], + initializers: %{}, + guid_ancestors: %{}, + guid_subscribers: %{}, + guid_received: %{}, + posts_in_flight: %{} ] @name __MODULE__ - def start_link(config) do - GenServer.start_link(__MODULE__, config, name: @name, timeout: timeout()) + 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(config) do + def ensure_started(opts \\ []) do case Process.whereis(@name) do - nil -> start_link(config) + 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 - type_id = GenServer.call(@name, {:browser_type_id, type}) - resp = sync_post(guid: type_id, method: "launch", params: Map.new(opts)) + 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 """ - Fire and forget. + Subscribe to messages for a guid and its descendants. """ - def post(msg) do - GenServer.cast(@name, {:post, msg}) + 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. - - We use double the default timeout if there is no message timeout, since some - playwright operations use a backoff internally ([100, 250, 500, 1000]). """ - def sync_post(msg) do - timeout = msg[:params][:timeout] || 2 * timeout() + 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, {:sync_post, msg}, timeout_with_grace_period) + GenServer.call(@name, {:post, msg}, timeout_with_grace_period) end @doc """ - Get all past responses for a playwright `guid` (e.g. a `Frame`). - The internal map used to track these responses is never cleaned, it will keep on growing. + 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 responses(guid) do - GenServer.call(@name, {:responses, guid}) + 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"} + msg = %{guid: "", params: %{sdkLanguage: "javascript"}, method: "initialize", metadata: %{}} PlaywrightPort.post(port, msg) {:ok, %__MODULE__{port: port}} end @impl GenServer - def handle_cast({:post, msg}, state) do - PlaywrightPort.post(state.port, msg) - {:noreply, state} + 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({:sync_post, msg}, from, state) do + 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, :pending_response, &Map.put(&1, msg.id, from))} + {: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({:responses, guid}, _from, state) do - {:reply, Map.get(state.responses, guid, []), state} + def handle_call({:initializer, guid}, _from, state) do + {:reply, Map.get(state.initializers, guid), state} end - def handle_call({:browser_type_id, type}, from, %{init: nil} = state) do - fun = &GenServer.reply(from, browser_type_id(&1, type)) - {:noreply, Map.update!(state, :pending_init, &[fun | &1])} + def handle_call(:awaiting_started, from, %{status: :pending} = state) do + {:noreply, Map.update!(state, :awaiting_started, &[from | &1])} end - def handle_call({:browser_type_id, type}, _from, state) do - {:reply, browser_type_id(state.init, type), state} + def handle_call(:awaiting_started, _from, %{status: :started} = state) do + {:reply, :ok, state} end @impl GenServer @@ -121,27 +138,86 @@ defmodule PhoenixTest.Playwright.Connection do {:noreply, state} end - defp handle_recv(%{params: %{type: "Playwright"}} = msg, state) do - init = msg.params.initializer - for fun <- state.pending_init, do: fun.(init) + 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 - %{state | init: init, pending_init: :done} + defp log_console(state, %{method: "console"} = msg) do + level = + case msg.params.type do + "error" -> :error + "debug" -> :debug + _ -> :info + end + + Logger.log(level, "Javascript console: #{msg.params.text}") + state end - defp handle_recv(msg, %{pending_response: pending} = state) when is_map_key(pending, msg.id) do - {from, pending} = Map.pop(pending, msg.id) + 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 | pending_response: pending} + %{state | posts_in_flight: in_flight} end - defp handle_recv(%{guid: guid} = msg, state) do - update_in(state.responses[guid], &[msg | &1 || []]) + 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 handle_recv(_msg, state), do: state + 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 browser_type_id(init, type), do: Map.fetch!(init, type).guid + defp notify_subscribers(state, _), do: state defp timeout do Application.get_env(:phoenix_test, :timeout_ms, @default_timeout_ms) diff --git a/lib/phoenix_test/playwright/frame.ex b/lib/phoenix_test/playwright/frame.ex index 13a52d31..93e0885f 100644 --- a/lib/phoenix_test/playwright/frame.ex +++ b/lib/phoenix_test/playwright/frame.ex @@ -8,41 +8,43 @@ defmodule PhoenixTest.Playwright.Frame do - https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/frame.ts """ - import PhoenixTest.Playwright.Connection, only: [sync_post: 1] + import PhoenixTest.Playwright.Connection, only: [post: 1] def goto(frame_id, url) do params = %{url: url} - sync_post(guid: frame_id, method: :goto, params: params) + post(guid: frame_id, method: :goto, params: params) :ok end def url(frame_id) do [guid: frame_id, method: :url, params: %{}] - |> sync_post() + |> post() |> unwrap_response(& &1.result.value) end def press(frame_id, selector, key) do params = %{selector: selector, key: key} - sync_post(guid: frame_id, method: :press, params: params) + post(guid: frame_id, method: :press, params: params) :ok end def title(frame_id) do [guid: frame_id, method: :title] - |> sync_post() + |> post() |> unwrap_response(& &1.result.value) end def expect(frame_id, params) do + params = Enum.into(params, %{isNot: false}) + [guid: frame_id, method: :expect, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1.result.matches) end def wait_for_selector(frame_id, params) do [guid: frame_id, method: :waitForSelector, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1.result.element) end @@ -50,13 +52,13 @@ defmodule PhoenixTest.Playwright.Frame do params = %{selector: selector} [guid: frame_id, method: :innerHTML, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1.result.value) end def content(frame_id) do [guid: frame_id, method: :content] - |> sync_post() + |> post() |> unwrap_response(& &1.result.value) end @@ -65,7 +67,7 @@ defmodule PhoenixTest.Playwright.Frame do params = Enum.into(opts, params) [guid: frame_id, method: :fill, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1) end @@ -74,7 +76,7 @@ defmodule PhoenixTest.Playwright.Frame do params = Enum.into(opts, params) [guid: frame_id, method: :selectOption, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1) end @@ -83,7 +85,7 @@ defmodule PhoenixTest.Playwright.Frame do params = Enum.into(opts, params) [guid: frame_id, method: :check, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1) end @@ -92,7 +94,7 @@ defmodule PhoenixTest.Playwright.Frame do params = Enum.into(opts, params) [guid: frame_id, method: :uncheck, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1) end @@ -101,7 +103,7 @@ defmodule PhoenixTest.Playwright.Frame do params = Enum.into(opts, params) [guid: frame_id, method: :setInputFiles, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1) end @@ -110,13 +112,13 @@ defmodule PhoenixTest.Playwright.Frame do params = Enum.into(opts, params) [guid: frame_id, method: :click, params: params] - |> sync_post() + |> post() |> unwrap_response(& &1) end defp unwrap_response(response, fun) do case response do - %{error: %{error: error}} -> {:error, error} + %{error: _} = error -> {:error, error} _ -> {:ok, fun.(response)} end end diff --git a/lib/phoenix_test/playwright/port.ex b/lib/phoenix_test/playwright/port.ex index e6193277..cc07869d 100644 --- a/lib/phoenix_test/playwright/port.ex +++ b/lib/phoenix_test/playwright/port.ex @@ -11,8 +11,9 @@ defmodule PhoenixTest.Playwright.Port do :buffer ] - def open(config) do - cli = Map.get_lazy(config, :driver_path, fn -> Application.fetch_env!(:phoenix_test, :playwright_cli) end) + def open(config \\ []) do + config = :phoenix_test |> Application.fetch_env!(:playwright) |> Keyword.merge(config) + cli = Keyword.fetch!(config, :cli) unless File.exists?(cli) do msg = """ @@ -20,7 +21,7 @@ defmodule PhoenixTest.Playwright.Port do To resolve this please 1. Install playwright, e.g. `npm i playwright` - 2. Configure the path correctly, e.g. in `config/text.exs`: `config :phoenix_test, playwright_cli: "assets/node_modules/playwright/cli.js"` + 2. Configure the path correctly, e.g. in `config/text.exs`: `config :phoenix_test, playwright: [cli: "assets/node_modules/playwright/cli.js"]` """ raise ArgumentError, msg @@ -33,8 +34,7 @@ defmodule PhoenixTest.Playwright.Port do end def post(state, msg) do - default = %{params: %{}, metadata: %{}} - frame = msg |> Enum.into(default) |> serialize() + frame = serialize(msg) length = byte_size(frame) padding = <> Port.command(state.port, padding <> frame) diff --git a/priv/static/assets/package-lock.json b/priv/static/assets/package-lock.json index 31a9962e..205bdcb6 100644 --- a/priv/static/assets/package-lock.json +++ b/priv/static/assets/package-lock.json @@ -6,8 +6,6 @@ "packages": { "": { "name": "phoenix_test", - "version": "1.0.0", - "license": "ISC", "dependencies": { "playwright": "^1.48.1" } @@ -26,11 +24,12 @@ } }, "node_modules/playwright": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", - "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0.tgz", + "integrity": "sha512-eKpmys0UFDnfNb3vfsf8Vx2LEOtflgRebl0Im2eQQnYMA4Aqd+Zw8bEOB+7ZKvN76901mRnqdsiOGKxzVTbi7A==", + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.48.1" + "playwright-core": "1.49.0" }, "bin": { "playwright": "cli.js" @@ -43,9 +42,10 @@ } }, "node_modules/playwright-core": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", - "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0.tgz", + "integrity": "sha512-R+3KKTQF3npy5GTiKH/T+kdhoJfJojjHESR1YEWhYuEKRVfVaxH3+4+GvXE5xyCngCxhxnykk0Vlah9v8fs3jA==", + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, From 61cc8522967389a722b6ff5a30aa9c215f429fcd Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Tue, 26 Nov 2024 10:45:10 +0100 Subject: [PATCH 6/6] Install browsers --only-shell --- .github/workflows/ci.yml | 132 +++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9cdd985..41517248 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: MIX_ENV: test @@ -31,68 +31,68 @@ jobs: name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: - otp: ['26.1.2'] - elixir: ['1.15.0', '1.16.0', '1.17.0', 'main'] + otp: ["26.1.2"] + elixir: ["1.15.0", "1.16.0", "1.17.0", "main"] steps: - - name: Set up Elixir - uses: erlef/setup-beam@v1 - with: - otp-version: ${{matrix.otp}} - elixir-version: ${{matrix.elixir}} - - - name: Checkout code - uses: actions/checkout@v3 - - - name: Cache deps - id: cache-deps - uses: actions/cache@v3 - env: - cache-name: cache-elixir-deps - with: - path: deps - key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} - restore-keys: | - ${{ runner.os }}-mix-${{ env.cache-name }}- - - - name: Cache compiled build - id: cache-build - uses: actions/cache@v3 - env: - cache-name: cache-compiled-build - with: - path: _build - key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} - restore-keys: | - ${{ runner.os }}-mix-${{ env.cache-name }}- - ${{ runner.os }}-mix- - - - name: Bust cache on job rerun to rule out incremental build as a source of flakiness - if: github.run_attempt != '1' - run: | - mix deps.clean --all - mix clean - shell: sh - - - name: Install dependencies - run: mix deps.get - - - name: Install JS dependencies - run: npm ci --prefix priv/static/assets - - - name: Compiles without warnings - run: mix compile --warnings-as-errors - - - name: Check Formatting - run: mix format --check-formatted - - - name: Build assets for browser tests - run: mix do assets.setup, assets.build - - - name: Setup database - run: mix ecto.setup - - - name: Install playwright browsers - run: npm exec --prefix priv/static/assets playwright install --with-deps - - - name: Run tests - run: "mix test || if [[ $? = 2 ]]; then mix test --failed; else false; fi" + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Cache deps + id: cache-deps + uses: actions/cache@v3 + env: + cache-name: cache-elixir-deps + with: + path: deps + key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-${{ env.cache-name }}- + + - name: Cache compiled build + id: cache-build + uses: actions/cache@v3 + env: + cache-name: cache-compiled-build + with: + path: _build + key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-${{ env.cache-name }}- + ${{ runner.os }}-mix- + + - name: Bust cache on job rerun to rule out incremental build as a source of flakiness + if: github.run_attempt != '1' + run: | + mix deps.clean --all + mix clean + shell: sh + + - name: Install dependencies + run: mix deps.get + + - name: Install JS dependencies + run: npm ci --prefix priv/static/assets + + - name: Compiles without warnings + run: mix compile --warnings-as-errors + + - name: Check Formatting + run: mix format --check-formatted + + - name: Build assets for browser tests + run: mix do assets.setup, assets.build + + - name: Setup database + run: mix ecto.setup + + - name: Install playwright browsers + run: npm exec --prefix priv/static/assets playwright install --with-deps --only-shell + + - name: Run tests + run: "mix test || if [[ $? = 2 ]]; then mix test --failed; else false; fi"