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..72313cb8 100644 --- a/lib/phoenix_test/assertions.ex +++ b/lib/phoenix_test/assertions.ex @@ -285,7 +285,7 @@ defmodule PhoenixTest.Assertions do |> append_found(found) end - defp assert_not_found_error_msg(selector, opts, other_matches \\ []) do + def assert_not_found_error_msg(selector, opts, other_matches \\ []) do count = Keyword.get(opts, :count, :any) position = 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..5480bbe1 --- /dev/null +++ b/lib/phoenix_test/playwright.ex @@ -0,0 +1,265 @@ +defmodule PhoenixTest.Playwright do + @moduledoc false + + alias ExUnit.AssertionError + alias PhoenixTest.Assertions + alias PhoenixTest.OpenBrowser + alias PhoenixTest.Playwright.Connection + alias PhoenixTest.Playwright.Frame + alias PhoenixTest.Playwright.Selector + + defstruct [:frame_id, :last_input_selector, within: :none] + + @endpoint Application.compile_env(:phoenix_test, :endpoint) + @default_timeout :timer.seconds(5) + + def build(frame_id) do + %__MODULE__{frame_id: frame_id} + 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, selector, opts \\ []) do + unless found?(session, selector, opts) do + raise(AssertionError, Assertions.assert_not_found_error_msg(selector, opts, [])) + end + + session + end + + def refute_has(session, selector, opts \\ []) do + if found?(session, selector, opts, isNot: true) do + raise(AssertionError, Assertions.refute_found_error_msg(selector, opts, [])) + end + + session + end + + defp found?(session, selector, opts, query_attrs \\ []) do + # TODO Support count + if opts[:count], do: raise("count not implemented") + + selector = + session + |> maybe_within() + |> Selector.concat(Selector.css_or_locator(selector)) + |> Selector.concat(Selector.text(opts[:text], opts)) + |> Selector.concat(Selector.at(opts[:at] || 0)) + + query = + Enum.into(query_attrs, %{ + expression: "to.be.visible", + isNot: false, + selector: selector, + timeout: timeout(opts) + }) + + {:ok, found?} = Frame.expect(session.frame_id, query) + found? + end + + def render_page_title(session) do + {:ok, title} = Frame.title(session.frame_id) + title + 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(css_selector) + + 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(css_selector) + + 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(input_selector, label) + + %{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, selector, label \\ nil) do + case result do + {:error, %{name: "TimeoutError"}} -> + msg = + case label do + nil -> "Could not find element with selector #{inspect(selector)}." + _ -> "Could not find element with label #{inspect(label)}." + end + + raise(ArgumentError, msg) + + {:error, %{name: "Error", message: "Error: strict mode violation" <> _}} -> + raise(ArgumentError, "Found more than one element with selector #{inspect(selector)}") + + {: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 + defdelegate assert_path(session, path), to: Assertions + defdelegate assert_path(session, path, opts), to: Assertions + defdelegate refute_path(session, path), to: Assertions + defdelegate refute_path(session, path, opts), to: Assertions +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..e6a18c37 --- /dev/null +++ b/lib/phoenix_test/playwright/frame.ex @@ -0,0 +1,110 @@ +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_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_result(:value) + end + + def expect(frame_id, params) do + [guid: frame_id, method: :expect, params: params] + |> sync_post() + |> unwrap_result(:matches) + end + + def inner_html(frame_id, selector) do + params = %{selector: selector} + + [guid: frame_id, method: :innerHTML, params: params] + |> sync_post() + |> unwrap_result(:value) + end + + def content(frame_id) do + [guid: frame_id, method: :content] + |> sync_post() + |> unwrap_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_result(:value) + 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_result(:values) + 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_result(:value) + 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_result(:value) + 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_result(:value) + 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_result(:value) + end + + defp unwrap_result(response, result_field) do + case response do + %{result: result} -> {:ok, Map.fetch!(result, result_field)} + %{error: %{error: error}} -> {:error, error} + other -> {:ok, other} + 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..6666449a 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,29 @@ defmodule PhoenixTest.AssertionsTest do end end + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented test "succeeds if element searched is title (Static)", %{conn: conn} do conn |> visit("/page/index") |> assert_has("title") end + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented test "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 +57,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 +66,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 +75,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 +83,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,6 +111,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :different_error_message test "raises error if element cannot be found but selector matches other elements", %{ conn: conn } do @@ -130,24 +133,32 @@ defmodule PhoenixTest.AssertionsTest do end end + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented 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 + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented 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 + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented test "can assert title's exactness", %{conn: conn} do conn |> visit("/live/index") |> assert_has("title", text: "PhoenixTest is the best!", exact: true) end + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented test "raises if title does not match expected value (Static)", %{conn: conn} do msg = ignore_whitespace(""" @@ -161,6 +172,8 @@ defmodule PhoenixTest.AssertionsTest do end end + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented test "raises if title does not match expected value (Live)", %{conn: conn} do msg = ignore_whitespace(""" @@ -174,6 +187,8 @@ defmodule PhoenixTest.AssertionsTest do end end + # Playwright: Implement special "title" assertion + @tag playwright: false, reason: :not_implemented test "raises if title is contained but is not exactly the same as expected (with exact=true)", %{conn: conn} do msg = @@ -188,6 +203,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :different_error_message test "raises error if element cannot be found and selector matches a nested structure", %{ conn: conn } do @@ -217,6 +233,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :not_implemented test "accepts a `count` option", %{conn: conn} do conn |> visit("/page/index") @@ -226,6 +243,7 @@ defmodule PhoenixTest.AssertionsTest do |> assert_has("h1", text: "Main page", count: 1) end + @tag playwright: false, reason: :not_implemented test "raises an error if count is more than expected count", %{conn: conn} do session = visit(conn, "/page/index") @@ -241,6 +259,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :not_implemented test "raises an error if count is less than expected count", %{conn: conn} do session = visit(conn, "/page/index") @@ -256,13 +275,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 + @tag playwright: false, reason: :different_error_message test "raises if `exact` text doesn't match", %{conn: conn} do msg = ignore_whitespace(""" @@ -282,13 +302,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,27 +327,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 + @tag playwright: false, reason: :not_implemented test "accepts a `count` option", %{conn: conn} do conn |> visit("/page/index") @@ -333,6 +358,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has(".multiple_links", text: "Multiple links", count: 1) end + @tag playwright: false, reason: :different_error_message test "raises if element is found", %{conn: conn} do msg = ignore_whitespace(""" @@ -352,6 +378,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :not_implemented test "raises if title is found", %{conn: conn} do msg = ignore_whitespace(""" @@ -365,6 +392,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :different_error_message test "raises an error if multiple elements are found", %{conn: conn} do conn = visit(conn, "/page/index") @@ -380,6 +408,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :not_implemented test "raises if there is one element and count is 1", %{conn: conn} do conn = visit(conn, "/page/index") @@ -393,6 +422,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :not_implemented test "raises if there are the same number of elements as refuted", %{conn: conn} do conn = visit(conn, "/page/index") @@ -410,6 +440,7 @@ defmodule PhoenixTest.AssertionsTest do end describe "refute_has/3" do + @tag playwright: false, reason: :not_implemented test "can be used to refute on page title (Static)", %{conn: conn} do conn |> visit("/page/index") @@ -417,6 +448,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has("title", text: "Not this title either") end + @tag playwright: false, reason: :not_implemented test "can be used to refute on page title (Live)", %{conn: conn} do conn |> visit("/live/index") @@ -424,12 +456,14 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has("title", text: "Not this title either") end + @tag playwright: false, reason: :not_implemented 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 + @tag playwright: false, reason: :not_implemented test "raises if title matches value (Static)", %{conn: conn} do msg = ignore_whitespace(""" @@ -443,6 +477,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :not_implemented test "raises if title matches value (Live)", %{conn: conn} do msg = ignore_whitespace(""" @@ -456,7 +491,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 +500,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,6 +509,7 @@ defmodule PhoenixTest.AssertionsTest do |> refute_has("#title", text: "Not main page") end + @tag playwright: false, reason: :different_error_message test "raises an error if one element is found", %{conn: conn} do conn = visit(conn, "/page/index") @@ -493,6 +529,7 @@ defmodule PhoenixTest.AssertionsTest do end end + @tag playwright: false, reason: :different_error_message test "raises an error if multiple elements are found", %{conn: conn} do conn = visit(conn, "/page/index") @@ -516,12 +553,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 + @tag playwright: false, reason: :different_error_message test "raises if `exact` text makes refutation false", %{conn: conn} do msg = ignore_whitespace(""" @@ -541,18 +579,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 +616,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 +647,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 +660,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 +675,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 +700,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..51d4c376 100644 --- a/test/phoenix_test/live_test.exs +++ b/test/phoenix_test/live_test.exs @@ -1,17 +1,15 @@ 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 + @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("/live/index") @@ -20,6 +18,8 @@ defmodule PhoenixTest.LiveTest do assert title == "PhoenixTest is the best!" end + # Playwright: not updated immediately (use assert_has("title") instead) + @tag playwright: false, reason: :irrelevant test "renders updated page title", %{conn: conn} do title = conn @@ -30,6 +30,8 @@ defmodule PhoenixTest.LiveTest do assert title == "Title changed!" end + # Playwright: returns "" instead of nil + @tag playwright: false, reason: :known_inconsistency test "returns nil if page title isn't found", %{conn: conn} do title = conn @@ -41,24 +43,25 @@ defmodule PhoenixTest.LiveTest do 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 +72,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 +81,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 +128,7 @@ defmodule PhoenixTest.LiveTest do end) end + @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,6 +137,7 @@ defmodule PhoenixTest.LiveTest do end end + @tag playwright: false, reason: :different_error_message 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 @@ -139,6 +146,7 @@ defmodule PhoenixTest.LiveTest do end end + @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 +157,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 +175,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 +186,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 +194,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 +206,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 +229,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 +237,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 +245,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 +253,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 +265,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 +273,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 +284,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 +300,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 +309,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 +330,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 +348,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 +358,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 +370,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 +378,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 +388,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 +418,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 +427,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,6 +437,7 @@ defmodule PhoenixTest.LiveTest do end end + @tag playwright: false, reason: :different_error_message 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/ @@ -427,21 +450,23 @@ 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']") + |> submit() + |> assert_has("#form-data", text: "race: 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']") + |> submit() + |> assert_has("#form-data", text: "race: 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 +474,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 +482,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 +491,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 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 +520,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 +531,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 +539,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 +548,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 +556,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 +564,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 +573,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 +582,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 +591,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 +602,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 +610,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 +619,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 +628,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 +639,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 +650,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 +662,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 +673,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 +681,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 +706,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 +717,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 +728,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 +739,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 +750,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 +761,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 +778,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 +796,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 +808,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 +828,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 +836,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 +844,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 +855,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 +866,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 +882,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 -> @@ -871,11 +890,11 @@ defmodule PhoenixTest.LiveTest do |> fill_in("Name", with: "Aragorn") |> submit() end) - |> assert_path("/page/create_record") |> assert_has("#form-data", text: "name: Aragorn") |> 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 +905,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 +930,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 +940,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 +952,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") @@ -943,19 +966,19 @@ defmodule PhoenixTest.LiveTest do end describe "current_path" do - test "it is set on visit", %{conn: conn} do + also_test_js "it is set on visit", %{conn: conn} do session = visit(conn, "/live/index") assert PhoenixTest.Driver.current_path(session) == "/live/index" end - test "it is set on visit with query string", %{conn: conn} do + also_test_js "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" end - test "it is updated on href navigation", %{conn: conn} do + also_test_js "it is updated on href navigation", %{conn: conn} do session = conn |> visit("/live/index") @@ -964,7 +987,9 @@ defmodule PhoenixTest.LiveTest do assert PhoenixTest.Driver.current_path(session) == "/page/index?details=true&foo=bar" end - test "it is updated on live navigation", %{conn: conn} do + # Playwright: not updated immediately (use assert_path instead) + @tag playwright: false, reason: :irrelevant + also_test_js "it is updated on live navigation", %{conn: conn} do session = conn |> visit("/live/index") @@ -973,7 +998,7 @@ defmodule PhoenixTest.LiveTest do assert PhoenixTest.Driver.current_path(session) == "/live/page_2?details=true&foo=bar" end - test "it is updated on live patching", %{conn: conn} do + also_test_js "it is updated on live patching", %{conn: conn} do session = conn |> visit("/live/index") @@ -982,7 +1007,9 @@ defmodule PhoenixTest.LiveTest do assert PhoenixTest.Driver.current_path(session) == "/live/index?details=true&foo=bar" end - test "it is updated on push navigation", %{conn: conn} do + # Playwright: not updated immediately (use assert_path instead) + @tag playwright: false, reason: :irrelevant + also_test_js "it is updated on push navigation", %{conn: conn} do session = conn |> visit("/live/index") @@ -991,7 +1018,7 @@ defmodule PhoenixTest.LiveTest do assert PhoenixTest.Driver.current_path(session) == "/live/page_2?foo=bar" end - test "it is updated on push patch", %{conn: conn} do + also_test_js "it is updated on push patch", %{conn: conn} do session = conn |> visit("/live/index") @@ -1002,20 +1029,28 @@ defmodule PhoenixTest.LiveTest do 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 +1064,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 +1084,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..0ba595b6 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,6 +16,8 @@ defmodule PhoenixTest.StaticTest do assert title == "PhoenixTest is the best!" end + # Playwright: returns "" instead of nil + @tag playwright: false, reason: :known_inconsistency test "renders nil if there's no page title", %{conn: conn} do title = conn @@ -29,18 +29,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 +52,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 +61,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,20 +94,21 @@ 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 - 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_link("Data-method Delete") + |> click_link("Data_method Delete") |> 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") + |> 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,8 @@ defmodule PhoenixTest.StaticTest do end end - test "raises an error when label is found but no corresponding input is found", %{conn: conn} do + @tag playwright: false, reason: :different_error_message + 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 +460,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 +468,22 @@ 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']") + |> submit() + |> assert_has("#form-data", text: "race: 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 +491,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,14 +499,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 - 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 -> @@ -506,6 +516,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "pet: dog") end + @tag playwright: false, reason: :not_implemented test "can target an option's text with exact_option: false", %{conn: conn} do conn |> visit("/page/index") @@ -516,7 +527,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "race: human") 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("/page/index") |> within("#same-labels", fn session -> @@ -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 calls matters) + @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,15 +801,7 @@ defmodule PhoenixTest.StaticTest do |> assert_has("#form-data", text: "country: Arnor") end - test "updates current_path on submit", %{conn: conn} do - conn - |> visit("/page/index") - |> fill_in("First Name", with: "Aragorn") - |> submit() - |> 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 +812,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 +828,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 +839,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 +881,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 +894,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 +906,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") @@ -905,18 +918,20 @@ defmodule PhoenixTest.StaticTest do end describe "current_path" do - test "it is set on visit", %{conn: conn} do + also_test_js "it is set on visit", %{conn: conn} do session = visit(conn, "/page/index") assert PhoenixTest.Driver.current_path(session) == "/page/index" end - test "it includes query string if available", %{conn: conn} do + also_test_js "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" end + # Playwright: not updated immediately (use assert_path instead) + @tag playwright: false, reason: :irrelevant test "it is updated on href navigation", %{conn: conn} do session = conn @@ -926,7 +941,7 @@ defmodule PhoenixTest.StaticTest do assert PhoenixTest.Driver.current_path(session) == "/page/page_2?foo=bar" end - test "it is updated on redirects", %{conn: conn} do + also_test_js "it is updated on redirects", %{conn: conn} do session = conn |> visit("/page/index") @@ -937,6 +952,7 @@ defmodule PhoenixTest.StaticTest do 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..12fe647e 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,11 +406,9 @@ defmodule PhoenixTest.IndexLive do
- - +
Choose a pet:
+ +
Select to get second breakfast: @@ -422,6 +420,7 @@ defmodule PhoenixTest.IndexLive do id="second-breakfast" name="second-breakfast" value="second-breakfast" + phx-update="ignore" /> @@ -466,6 +465,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 +477,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 +495,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 +575,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/page_view.ex b/test/support/page_view.ex index e13656c8..caf7d212 100644 --- a/test/support/page_view.ex +++ b/test/support/page_view.ex @@ -66,13 +66,13 @@ defmodule PhoenixTest.PageView do data-to="/page/delete_record" data-csrf="sometoken" > - Data-method Delete + Data_method Delete 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())