View Source PhoenixTest.Playwright (PhoenixTestPlaywright v0.1.5)
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.exsdeps:{: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
endAs 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: 2000Playwright Traces
You can enable trace recording in different ways:
- Environment variable, see Configuration
- ExUnit
@tag :trace - ExUnit
@tag trace: :opento open the trace viewer automatically after completion
Common problems
- Test failures in CI (timeouts): Try less concurrency, e.g.
mix test --max-cases 1for GitHub CI shared runners - LiveView not connected: add
assert_has("body .phx-connected")to test aftervisiting (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: trueAdvanced 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