View Source PhoenixTest.Playwright (PhoenixTestPlaywright v0.2.0)
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"]
- 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"
],
timeout_ms: 2000
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