View Source PhoenixTest.Playwright (PhoenixTestPlaywright v0.2.1)
Warning
This driver is experimental.
If you don't need browser based tests, see PhoenixTest
on regular usage.
Execute PhoenixTest cases in an actual browser via Playwright.
Example
Refer to the accompanying example repo for a full example: https://github.com/ftes/phoenix_test_playwright_example/commits/main
Setup
- Add to
mix.exs
deps:{:phoenix_test_playwright, "~> 0.1", only: :test, runtime: false}
- Install Playwright:
npm --prefix assets i -D playwright
- Install browsers:
npm --prefix assets exec playwright install --with-deps
- Add to
config/test.exs
:config :phoenix_test, otp_app: :your_app, playwright: [cli: "assets/node_modules/playwright/cli.js"]
(for more options, see Configuration below) - Add to
config/test.exs
:config :your_app, YourAppWeb.Endpoint, server: true
- Add to
test/test_helpers.exs
:Application.put_env(:phoenix_test, :base_url, YourAppWeb.Endpoint.url())
Usage
defmodule MyFeatureTest do
use PhoenixTest.Case, async: true
@moduletag :playwright
@tag trace: :open
test "heading", %{conn: conn} do
conn
|> visit("/")
|> assert_has("h1", text: "Heading")
end
end
As shown above, you can use ExUnit.Case
parameterized tests
to run tests concurrently in different browsers.
Configuration
In config/test.exs
:
config :phoenix_test,
otp_app: :your_app,
playwright: [
cli: "assets/node_modules/playwright/cli.js",
browser: [browser: :chromium, headless: System.get_env("PLAYWRIGHT_HEADLESS", "t") in ~w(t true)],
trace: System.get_env("PLAYWRIGHT_TRACE", "false") in ~w(t true),
trace_dir: "tmp",
js_logger:
# Default to true if you like seeing log messages for errors during test
if System.get_env("PLAYWRIGHT_LOG_JS_MESSSAGES", "false") in ~w(t true) do
:default
end
],
timeout_ms: 2000
JavaScript console messages are are written to standard IO and standard error by default.
You can set the :js_logger
config to nil
to silence them.
Note that the same options you pass to :playwright
in your global configuration can also
be passed to the @tag
in your test module. Thus, you might set the global default to
headless: false
, then override it for a single module by setting up your test like this:
defmodule DebuggingFeatureTest do
use PhoenixTest.Case, async: true
# Run this module's tests in a headed browser, with a 1000 millisecond
# pause between browser interactions.
@moduletag playwright: [headless: false, slowMo: 1_000]
...
end
Playwright Traces
You can enable trace recording in different ways:
- Environment variable, see Configuration
- ExUnit
@tag :trace
- ExUnit
@tag trace: :open
to open the trace viewer automatically after completion
Common problems
- Test failures in CI (timeouts): Try less concurrency, e.g.
mix test --max-cases 1
for GitHub CI shared runners - LiveView not connected: add
assert_has("body .phx-connected")
to test aftervisit
ing (or otherwise navigating to) a LiveView - LiveComponent not connected: add
data-connected={connected?(@socket)}
to template andassert_has("#my-component[data-connected]")
to test
Ecto SQL.Sandbox
PhoenixTest.Case
automatically takes care of this.
It passes a user agent referencing your Ecto repos.
This allows for concurrent browser tests.
Make sure to follow the advanced set up instructions if necessary:
defmodule MyTest do
use PhoenixTest.Case, async: true
Advanced assertions
def assert_has_value(session, label, value, opts \\ []) do
opts = Keyword.validate!(opts, exact: true)
assert_found(session,
selector: Selector.label(label, opts),
expression: "to.have.value",
expectedText: [%{string: value}]
)
end
def assert_has_selected(session, label, value, opts \\ []) do
opts = Keyword.validate!(opts, exact: true)
assert_found(session,
selector: label |> Selector.label(opts) |> Selector.concat("option[selected]"),
expression: "to.have.text",
expectedText: [%{string: value}]
)
end
def assert_is_chosen(session, label, opts \\ []) do
opts = Keyword.validate!(opts, exact: true)
assert_found(session,
selector: Selector.label(label, opts),
expression: "to.have.attribute",
expressionArg: "checked"
)
end
def assert_is_editable(session, label, opts \\ []) do
opts = Keyword.validate!(opts, exact: true)
assert_found(session,
selector: Selector.label(label, opts),
expression: "to.be.editable"
)
end
def refute_is_editable(session, label, opts \\ []) do
opts = Keyword.validate!(opts, exact: true)
assert_found(
session,
[
selector: Selector.label(label, opts),
expression: "to.be.editable"
],
is_not: true
)
end
def assert_found(session, params, opts \\ []) do
is_not = Keyword.get(opts, :is_not, false)
params = Enum.into(params, %{isNot: is_not})
unwrap(session, fn frame_id ->
{:ok, found} = Frame.expect(frame_id, params)
if is_not, do: refute(found), else: assert(found)
end)
end
def assert_download(session, name, contains: content) do
assert_receive({:playwright, %{method: "download"} = download_msg}, 2000)
artifact_guid = download_msg.params.artifact.guid
assert_receive({:playwright, %{method: "__create__", params: %{guid: ^artifact_guid}} = artifact_msg}, 2000)
download_path = artifact_msg.params.initializer.absolutePath
wait_for_file(download_path)
assert download_msg.params.suggestedFilename =~ name
assert File.read!(download_path) =~ content
session
end
defp wait_for_file(path, remaining_ms \\ 2000, wait_for_ms \\ 100)
defp wait_for_file(path, remaining_ms, _) when remaining_ms <= 0, do: flunk("File #{path} does not exist")
defp wait_for_file(path, remaining_ms, wait_for_ms) do
if File.exists?(path) do
:ok
else
Process.sleep(wait_for_ms)
wait_for_file(path, remaining_ms - wait_for_ms, wait_for_ms)
end
end