View Source PhoenixTest (PhoenixTest v0.3.0)
PhoenixTest provides a unified way of writing feature tests -- regardless of whether you're testing LiveView pages or static pages.
It also handles navigation between LiveView and static pages seamlessly. So, you don't have to worry about what type of page you're visiting. Just write the tests from the user's perspective.
Thus, you can test a flow going from static to LiveView pages and back without having to worry about the underlying implementation.
This is a sample flow:
test "admin can create a user", %{conn: conn} do
conn
|> visit("/")
|> click_link("Users")
|> fill_in("Name", with: "Aragorn")
|> choose("Ranger")
|> assert_has(".user", text: "Aragorn")
end
Note that PhoenixTest does not handle JavaScript. If you're looking for something that supports JavaScript, take a look at Wallaby.
Setup
PhoenixTest requires Phoenix 1.7+
and LiveView 0.20+
. It may work with
earlier versions, but I have not tested that.
Installation
Add phoenix_test
to your list of dependencies in mix.exs
:
def deps do
[
{:phoenix_test, "~> 0.3.0", only: :test, runtime: false}
]
end
Configuration
In config/test.exs
specify the endpoint to be used for routing requests:
config :phoenix_test, :endpoint, MyAppWeb.Endpoint
Getting PhoenixTest
helpers
PhoenixTest
helpers can be included via import PhoenixTest
.
But since each test needs a conn
struct to get started, you'll likely want
to set up a few things before that.
There are two ways to do that.
With ConnCase
If you plan to use ConnCase
solely for PhoenixTest
, then you can import
the helpers there:
using do
quote do
# importing other things for ConnCase
import PhoenixTest
# doing other setup for ConnCase
end
end
Adding a FeatureCase
If you want to create your own FeatureCase
helper module like ConnCase
,
you can copy the code below which can be use
d from your tests (replace
MyApp
with your app's name):
defmodule MyAppWeb.FeatureCase do
use ExUnit.CaseTemplate
using do
quote do
use MyAppWeb, :verified_routes
import MyAppWeb.FeatureCase
import PhoenixTest
end
end
setup tags do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end
Note that we assume your Phoenix project is using Ecto and its phenomenal
SQL.Sandbox
. If it doesn't, feel free to remove the SQL.Sandbox
code
above.
Usage
Now that we have all the setup out of the way, we can create tests like this:
# test/my_app_web/features/admin_can_create_user_test.exs
defmodule MyAppWeb.AdminCanCreateUserTest do
use MyAppWeb.FeatureCase, async: true
test "admin can create user", %{conn: conn} do
conn
|> visit("/")
|> click_link("Users")
|> fill_in("Name", with: "Aragorn")
|> fill_in("Email", with: "aragorn@dunedain.com")
|> click_button("Create")
|> assert_has(".user", text: "Aragorn")
end
end
Filling out forms
We can fill out forms by targetting their inputs, selects, etc. by label:
test "admin can create user", %{conn: conn} do
conn
|> visit("/")
|> click_link("Users")
|> fill_in("Name", with: "Aragorn")
|> select("Elessar", from: "Aliases")
|> choose("Human") # <- choose a radio option
|> check("Ranger") # <- check a checkbox
|> click_button("Create")
|> assert_has(".user", text: "Aragorn")
end
For more info, see fill_in/3
, select/3
, choose/2
, check/2
,
uncheck/2
.
Submitting forms without clicking a button
Once we've filled out a form, you can click a button with
click_button/2
to submit the form. But sometimes you want to emulate what
would happen by just pressing <Enter>.
For that case, you can use submit/1
to submit the form you just filled
out.
session
|> fill_in("Name", with: "Aragorn")
|> check("Ranger")
|> submit()
For more info, see submit/1
.
Targeting which form to fill out
If you find yourself in a situation where you have multiple forms with the same labels (even when those labels point to different inputs), then you might have to scope your form-filling.
To do that, you can scope all of the form helpers using within/3
:
session
|> within("#user-form", fn session ->
session
|> fill_in("Name", with: "Aragorn")
|> check("Ranger")
|> click_button("Create")
end)
For more info, see within/3
.
Summary
Functions
Assert helper to ensure an element with given CSS selector is present.
Assert helper to ensure an element with given CSS selector and options.
Assert helper to verify current request path. Takes an optional query_params
map.
Same as assert_path/2
but takes an optional query_params
map.
Check a checkbox.
Choose a radio button option.
Perfoms action defined by button (and based on attributes present).
Performs action defined by button with CSS selector and text.
Clicks a link with given text and performs the action.
Clicks a link with given CSS selector and text and performs the action. selector to target the link.
Fills text inputs and textareas, targetting the elements by their labels.
Open the default browser to display current HTML of session
.
Opposite of assert_has/2
helper. Verifies that element with
given CSS selector is not present.
Opposite of assert_has/3
helper. Verifies that element with
given CSS selector and text
is not present.
Verifies current request path is NOT the one provided. Takes an optional
query_params
map for more specificity.
Same as refute_path/2
but takes an optional query_params
for more specific
refutation.
Selects an option from a select dropdown.
Uncheck a checkbox.
Escape hatch to give users access to underlying "native" data structure.
Entrypoint to create a session.
Helpers to scope filling out form within a given selector. Use this if you have more than one form on a page with similar labels.
Functions
Assert helper to ensure an element with given CSS selector is present.
It'll raise an error if no elements are found, but it will not raise if more than one matching element is found.
If you want to specify the content of the element, use assert_has/3
.
Examples
# assert there's an h1
assert_has(session, "h1")
# assert there's an element with ID "user"
assert_has(session, "#user")
Assert helper to ensure an element with given CSS selector and options.
It'll raise an error if no elements are found, but it will not raise if more than one matching element is found.
Options
text
: the text filter to look for.exact
: by defaultassert_has/3
will perform a substring match (e.g.a =~ b
). That makes it easier to assert text within HTML elements that also contain other HTML elements. But sometimes we want to assert the exact text is present. For that, useexact: true
. (defaults tofalse
)count
: the number of items you expect to match CSS selector (andtext
if provided)at
: the element to be asserted against
Examples
# assert there's an element with ID "user" and text "Aragorn"
assert_has(session, "#user", text: "Aragorn")
# ^ succeeds if text found is "Aragorn" or "Aragorn, Son of Arathorn"
# assert there's an element with ID "user" and text "Aragorn"
assert_has(session, "#user", text: "Aragorn", exact: true)
# ^ succeeds only if text found is "Aragorn". Fails if finds "Aragorn, Son of Arathorn"
# assert there are two elements with class "posts"
assert_has(session, ".posts", count: 2)
# assert there are two elements with class "posts" and text "Hello"
assert_has(session, ".posts", text: "Hello", count: 2)
# assert the second element in the list of ".posts" has text "Hello"
assert_has(session, ".posts", at: 2, text: "Hello")
Assert helper to verify current request path. Takes an optional query_params
map.
Note on Live Patch Implementation
Capturing the current path in live patches relies on message passing and could, therefore, be subject to intermittent failures. Please open an issue if you see intermittent failures when using
assert_path
with live patches so we can improve the implementation.
Examples
# assert we're at /users
conn
|> visit("/users")
|> assert_path("/users")
# assert we're at /users?name=frodo
conn
|> visit("/users")
|> assert_path("/users", query_params: %{name: "frodo"})
Same as assert_path/2
but takes an optional query_params
map.
Check a checkbox.
If the form is a LiveView form, and if the form has a phx-change
attribute
defined, check/2
will trigger the phx-change
event.
This can be followed by a click_button/3
or submit/1
to submit the form.
Example
Given we have a form that contains this:
<input type="hidden" name="admin" value="off" />
<label for="admin">Admin</label>
<input id="admin" type="checkbox" name="admin" value="on" />
We can check the "Admin" option:
session
|> check("Admin")
Choose a radio button option.
If the form is a LiveView form, and if the form has a phx-change
attribute
defined, choose/3
will trigger the phx-change
event.
This can be followed by a click_button/3
or submit/1
to submit the form.
Example
Given we have a form that contains this:
<input type="radio" id="email" name="contact" value="email" />
<label for="email">Email</label>
<input type="radio" id="phone" name="contact" value="phone" />
<label for="phone">Phone</label>
<input type="radio" id="mail" name="contact" value="mail" checked />
<label for="mail">Mail</label>
We can choose to be contacted by email:
session
|> choose("Email")
Perfoms action defined by button (and based on attributes present).
This can be used in a number of ways.
Button with phx-click
If the button has a phx-click
on it, it'll send the event to the LiveView.
Example
<button phx-click="save">Save</button>
session
|> click_button("Save") # <- will send "save" event to LiveView
Button relying on Phoenix.HTML.js
If the button acts as a form via Phoenix.HTML's data-method
, data-to
, and
data-csrf
, this will emulate Phoenix.HTML.js and submit the form via data
attributes.
But note that this doesn't guarantee the JavaScript that handles form
submissions via data
attributes is loaded. The test emulates the behavior
but you must make sure the JavaScript is loaded.
For more on that, see https://hexdocs.pm/phoenix_html/Phoenix.HTML.html#module-javascript-library
Example
<button data-method="delete" data-to="/users/2" data-csrf="token">Delete</button>
session
|> click_button("Delete") # <- will submit form like Phoenix.HTML.js does
Combined with fill_in/3
, select/3
, etc.
This function can be preceded by filling out a form.
Example
session
|> fill_in("Name", name: "Aragorn")
|> check("Human")
|> click_button("Create")
Submitting default data
By default, using click_button/2
will submit the form it's part of (so long
as it has a phx-click
, data-*
attrs, or an action
).
It will also include any hidden inputs and default data (e.g. inputs with a
value
set and the button's name
and value
if present).
Example
<form method="post" action="/users/2">
<input type="hidden" name="admin" value="true"/>
<button name="complete" value="true">Complete</button>
</form>
session
|> click_button("Complete")
# ^ includes `%{"admin" => "true", "complete" => "true"}` in payload
Single-button forms
click_button/2
is smart enough to use a hidden input's value with
name=_method
as the method to send (e.g. when we want to send delete
,
put
, or patch
)
That means, it is helpful to submit single-button forms.
Example
<form method="post" action="/users/2">
<input type="hidden" name="_method" value="delete" />
<button>Delete</button>
</form>
session
|> click_button("Delete") # <- Triggers full form delete.
Performs action defined by button with CSS selector and text.
See click_button/2
for more details.
Clicks a link with given text and performs the action.
Here's how it handles different types of a
tags:
- With
href
: follows it to the next page - With
phx-click
: it'll send the event to the appropriate LiveView - With live redirect: it'll follow the live navigation to the next LiveView
- With live patch: it'll patch the current LiveView
Examples
<.link href="/page/2">Page 2</.link>
<.link phx-click="next-page">Next Page</.link>
<.link navigate="next-liveview">Next LiveView</.link>
<.link patch="page/details">Page Details</.link>
session
|> click_link("Page 2") # <- follows to next page
session
|> click_link("Next Page") # <- sends "next-page" event to LiveView
session
|> click_link("Next LiveView") # <- follows to next LiveView
session
|> click_link("Page Details") # <- applies live patch
Submitting forms
Phoenix allows for submitting forms on links via Phoenix.HTML's data-method
,
data-to
, and data-csrf
.
We can use click_link
to emulate Phoenix.HTML.js and submit the
form via data attributes.
But note that this doesn't guarantee the JavaScript that handles form
submissions via data
attributes is loaded. The test emulates the behavior
but you must make sure the JavaScript is loaded.
For more on that, see https://hexdocs.pm/phoenix_html/Phoenix.HTML.html#module-javascript-library
Example
<a href="/users/2" data-method="delete" data-to="/users/2" data-csrf="token">
Delete
</a>
session
|> click_link("Delete") # <- will submit form like Phoenix.HTML.js does
Clicks a link with given CSS selector and text and performs the action. selector to target the link.
See click_link/2
for more details.
Fills text inputs and textareas, targetting the elements by their labels.
If the form is a LiveView form, and if the form has a phx-change
attribute
defined, fill_in/3
will trigger the phx-change
event.
This can be followed by a click_button/3
or submit/1
to submit the form.
Examples
Given we have a form that contains this:
<label for="name">Name</label>
<input id="name" name="name"/>
or this:
<label>
Name
<input name="name"/>
</label>
We can fill in the name
field:
session
|> fill_in("Name", with: "Aragorn")
Open the default browser to display current HTML of session
.
Examples
session
|> visit("/")
|> open_browser()
|> submit_form("#user-form", name: "Aragorn")
Opposite of assert_has/2
helper. Verifies that element with
given CSS selector is not present.
It'll raise an error if any elements that match selector are found.
If you want to specify the content of the element, use refute_has/3
.
Example
# refute there's an h1
refute_has(session, "h1")
# refute there's an element with ID "user"
refute_has(session, "#user")
Opposite of assert_has/3
helper. Verifies that element with
given CSS selector and text
is not present.
It'll raise an error if any elements that match selector and options.
Options
text
: the text filter to look for.exact
: by defaultrefute_has/3
will perform a substring match (e.g.a =~ b
). That makes it easier to refute text within HTML elements that also contain other HTML elements. But sometimes we want to refute the exact text is absent. For that, useexact: true
.count
: the number of items you're expecting should not match the CSS selector (andtext
if provided)at
: the element to be refuted against
Examples
# refute there's an element with ID "user" and text "Aragorn"
refute_has(session, "#user", text: "Aragorn")
# refute there's an element with ID "user" and exact text "Aragorn"
refute_has(session, "#user", text: "Aragorn", exact: true)
# refute there are two elements with class "posts" (less or more will not raise)
refute_has(session, ".posts", count: 2)
# refute there are two elements with class "posts" and text "Hello"
refute_has(session, ".posts", text: "Hello", count: 2)
# refute the second element with class "posts" has text "Hello"
refute_has(session, ".posts", at: 2, text: "Hello")
Verifies current request path is NOT the one provided. Takes an optional
query_params
map for more specificity.
Note on Live Patch Implementation
Capturing the current path in live patches relies on message passing and could, therefore, be subject to intermittent failures. Please open an issue if you see intermittent failures when using
refute_path
with live patches so we can improve the implementation.
Examples
# refute we're at /posts
conn
|> visit("/users")
|> refute_path("/posts")
# refute we're at /users?name=frodo
conn
|> visit("/users?name=aragorn")
|> refute_path("/users", query_params: %{name: "frodo"})
Same as refute_path/2
but takes an optional query_params
for more specific
refutation.
Selects an option from a select dropdown.
If the form is a LiveView form, and if the form has a phx-change
attribute
defined, select/3
will trigger the phx-change
event.
This can be followed by a click_button/3
or submit/1
to submit the form.
Examples
Given we have a form that contains this:
<label for="race">Race</label>
<select id="race" name="race">
<option value="human">Human</option>
<option value="elf">Elf</option>
<option value="dwarf">Dwarf</option>
<option value="orc">Orc</option>
</select>
We can select an option:
session
|> select("Human", from: "Race")
Helper to submit a pre-filled form without clicking a button (see fill_in/3
,
select/3
, choose/2
, etc. for how to fill a form.)
Forms are typically submitted by clicking buttons. But sometimes we want to emulate what happens when we submit a form hitting <Enter>. That's what this helper does.
If the form is a LiveView form, and if the form has a phx-submit
attribute
defined, submit/1
will trigger the phx-submit
event. Otherwise, it'll
submit the form regularly.
If the form has a submit button with a name
and value
, submit/1
will
also include that data in the payload.
Example
session
|> fill_in("Name", with: "Aragorn")
|> select("Human", from: "Race")
|> choose("Email")
|> submit()
Uncheck a checkbox.
If the form is a LiveView form, and if the form has a phx-change
attribute
defined, uncheck/2
will trigger the phx-change
event.
This can be followed by a click_button/3
or submit/1
to submit the form.
Example
Given we have a form that contains this:
<input type="hidden" name="admin" value="off" />
<label for="admin">Admin</label>
<input id="admin" type="checkbox" name="admin" value="on" />
We can uncheck the "Admin" option:
session
|> uncheck("Admin")
Note that unchecking a checkbox in HTML doesn't actually send any data to the
server. That's why we have to have a hidden input with the default value (in
the example above: admin="off"
).
Escape hatch to give users access to underlying "native" data structure.
Once the unwrapped actions are performed, PhoenixTest will handle redirects (if any).
In LiveView tests,
unwrap/2
will pass theview
that comes from Phoenix.LiveViewTestlive/2
. Your action must return the result of arender_*
LiveViewTest action.In non-LiveView tests,
unwrap/2
will pass theconn
struct. And your action must return aconn
struct.
Examples
# in a LiveView
session
|> unwrap(fn view ->
view
|> LiveViewTest.element("#hook")
|> LiveViewTest.render_hook(:hook_event, %{name: "Legolas"})
end)
# in a non-LiveView
session
|> unwrap(fn conn ->
conn
|> Phoenix.ConnTest.recycle()
end)
Entrypoint to create a session.
visit/2
takes a Plug.Conn
struct and the path to visit.
It returns a session
which the rest of the PhoenixTest
functions can use.
Note that visit/2
is smart enough to know if the page you're visiting is a
LiveView or a static view. You don't need to worry about which type of page
you're visiting.
Helpers to scope filling out form within a given selector. Use this if you have more than one form on a page with similar labels.
Examples
Given we have some HTML like this:
<form id="user-form" action="/users" method="post">
<label for="name">Name</label>
<input id="name" name="name"/>
<input type="hidden" name="admin" value="off" />
<label for="admin">Admin</label>
<input id="admin" type="checkbox" name="admin" value="on" />
</form>
# and assume another form with "Name" and "Admin" labels
We can fill the form like this:
session
|> within("#user-form", fn session ->
session
|> fill_in("Name", with: "Aragorn")
|> check("Admin")
end)