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
end
Please 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-deps
Config
# config/test.exs config :phoenix_test, otp_app: :your_app config :your_app, YourAppWeb.Endpoint, server: true
Runtime 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_000
Traces
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" do
Automatically 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 viewport
Automatically 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 1
for 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 connect
LiveComponent 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 connect
Ecto SQL.Sandbox
defmodule MyTest do
use PhoenixTest.Case, async: true
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:
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, ArrowUp
Modifiers are also supported:
Shift, Control, Alt, Meta, ShiftLeft, ControlOrMeta
Combinations are also supported:
Control+o, Control++, Control+Shift+T
Options
: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")