View Source PhoenixTest.Playwright (PhoenixTestPlaywright v0.4.0)
Run feature tests in an actual browser, using PhoenixTest and Playwright.
defmodule Features.RegisterTest do
use PhoenixTest.Case, async: true,
# run in multiple browsers in parallel
parameterize: [%{browser: :chromium}, %{browser: :firefox}]
@moduletag :playwright
@moduletag headless: false # show browser window
@moduletag slow_mo: :timer.seconds(1) # add delay between interactions
@tag trace: :open # replay in interactive viewer
test "register", %{conn: conn} do
conn
|> visit(~p"/")
|> click_link("Register")
|> fill_in("Email", with: "f@ftes.de")
|> click_button("Create an account")
|> assert_has(".text-rose-600", text: "required")
|> screenshot("error.png", full_page: true)
end
endPlease get in touch with feedback of any shape and size.
Enjoy! Freddy.
Getting started
Add dependency
# mix.exs {:phoenix_test_playwright, "~> 0.4", only: :test, runtime: false}Install playwright and browser
npm --prefix assets i -D playwright npm --prefix assets exec playwright install chromium --with-depsConfig
# config/test.exs config :phoenix_test, otp_app: :your_app config :your_app, YourAppWeb.Endpoint, server: trueRuntime config
# test/test_helpers.exs Application.put_env(:phoenix_test, :base_url, YourAppWeb.Endpoint.url()Use in test
defmodule MyTest do use PhoenixTest.Case, async: true @moduletag :playwright test "in browser", %{conn: conn} do conn |> visit(~p"/") |> unwrap(&Frame.evaluate(&1.frame_id, "console.log('Hey')"))
Reference project
github.com/ftes/phoenix_test_playwright_example
The last commit adds a feature test for the phx gen.auth registration page
and runs it in CI (Github Actions).
Configuration
# config/test.ex
config :phoenix_test,
otp_app: :your_app,
playwright: [
browser: :chromium,
headless: System.get_env("PW_HEADLESS", "true") in ~w(t true),
js_logger: false,
screenshot: System.get_env("PW_SCREENSHOT", "false") in ~w(t true),
trace: System.get_env("PW_TRACE", "false") in ~w(t true),
]See PhoenixTest.Playwright.Config for more details.
You can override some options in your test via @moduletag/@describetag/@tag:
defmodule DebuggingFeatureTest do
use PhoenixTest.Case, async: true
# Run test in a browser with a 1 second delay between every interaction
@moduletag headless: false
@moduletag slow_mo: 1_000Traces
Playwright traces record a full browser history, including 'user' interaction, browser console, network transfers etc. Traces can be explored in an interactive viewer for debugging purposes.
Manually
@tag trace: :open
test "record a trace and open it automatically in the viewer" doAutomatically for failed tests in CI
# config/test.exs
config :phoenix_test, playwright: [trace: System.get_env("PW_TRACE", "false") in ~w(t true)]# .github/workflows/elixir.yml
run: "mix test || if [[ $? = 2 ]]; then PW_TRACE=true mix test --failed; else false; fi"Screenshots
Manually
|> visit(~p"/")
|> screenshot("home.png") # captures entire page by default, not just viewportAutomatically for failed tests in CI
# config/test.exs
config :phoenix_test, playwright: [screenshot: System.get_env("PW_SCREENSHOT", "false") in ~w(t true)]# .github/workflows/elixir.yml
run: "mix test || if [[ $? = 2 ]]; then PW_SCREENSHOT=true mix test --failed; else false; fi"Common problems
Test failure in CI (timeout)
- Limit concurrency:
mix test --max-cases 1for GitHub CI shared runners - Increase timemout:
config :phoenix_test, playwright: [timeout: :timer.seconds(2)]
LiveView not connected
|> visit(~p"/")
|> assert_has("body .phx-connected")
# now continue, playwright has waited for LiveView to connectLiveComponent not connected
<div id="my-component" data-connected={connected?(@socket)}`>|> visit(~p"/")
|> assert_has("#my-component[data-connected]")
# now continue, playwright has waited for LiveComponent to connectEcto SQL.Sandbox
defmodule MyTest do
use PhoenixTest.Case, async: truePhoenixTest.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:
Missing Playwright features
This driver doesn't wrap the entire Playwright API.
However, you should be able to wrap any missing functionality yourself
using PhoenixTest.unwrap/2, Frame, Selector,
and the Playwright code.
If you think others might benefit, please open a PR.
Here is some inspiration:
def assert_a11y(session) do
A11yAudit.Assertions.assert_no_violations(fn ->
Frame.evaluate(session.frame_id, A11yAudit.JS.axe_core())
session.frame_id
|> Frame.evaluate("axe.run().then(res => JSON.stringify(res))")
|> JSON.decode!()
|> A11yAudit.Results.from_json()
end)
session
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
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, %{is_not: 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
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
Summary
Functions
Focuses the matching element and presses a combination of the keyboard keys.
Takes a screenshot of the current page and saves it to the given file path.
Focuses the matching element and simulates user typing.
Functions
Focuses the matching element and presses a combination of the keyboard keys.
Examples of supported keys:
F1 - F12, Digit0- Digit9, KeyA- KeyZ, Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUpModifiers are also supported:
Shift, Control, Alt, Meta, ShiftLeft, ControlOrMetaCombinations are also supported:
Control+o, Control++, Control+Shift+TOptions
:delay(integer): Time to wait between keydown and keyup in milliseconds. Defaults to 0.
Examples
> PhoenixTest.Playwright.press(session, "#id", "Enter")
Takes a screenshot of the current page and saves it to the given file path.
The screenshot type will be inferred from the file extension on the path you provide.
If the path is relative (e.g., "my_screenshot.png" or "my_test/my_screenshot.jpg"), it will
be saved in the directory specified by the :screenshot_dir config option, which defaults
to "screenshots".
Options
:full_page(boolean): Whether to take a full page screenshot. If false, only the current viewport will be captured. Defaults to true.:omit_background(boolean): Whether to omit the background, allowing screenshots to be captured with transparency. Only applicable to PNG images. Defaults to false.
Examples
# By default, writes to screenshots/my-screenshot.png within your project root
> PhoenixTest.Playwright.screenshot(session, "my-screenshot.png")
# Writes to screenshots/my-test/my-screenshot.jpg by default
> PhoenixTest.Playwright.screenshot(session, "my-test/my-screenshot.jpg")
Focuses the matching element and simulates user typing.
In most cases, you should use fill_in/4 instead.
Options
:delay(integer): Time to wait between key presses in milliseconds. Defaults to 0.
Examples
> PhoenixTest.Playwright.type(session, "#id", "some text")