Wallaby helps you test your web applications by simulating user interactions. By default it runs each test case concurrently and manages browsers for you.
- Intuitive DSL for interacting with pages.
- Manages multiple browser processes.
- Works with Ecto's test Sandbox.
Add Wallaby to your list of dependencies in mix.exs
:
def deps do
[{:wallaby, "~> 0.16.1"}]
end
Then ensure that Wallaby is started in your test_helper.exs
:
{:ok, _} = Application.ensure_all_started(:wallaby)
If you're testing a Phoenix application with Ecto then you can enable concurrent testing by adding the Phoenix.Ecto.SQL.Sandbox
to your Endpoint
.
Note: This requires Ecto v2.0.0-rc.0 or newer.
Note 2: It's important that this is at the top of endpoint.ex
, before any other plugs.
# lib/endpoint.ex
defmodule YourApp.Endpoint do
use Phoenix.Endpoint, otp_app: :your_app
if Application.get_env(:your_app, :sql_sandbox) do
plug Phoenix.Ecto.SQL.Sandbox
end
Make sure Phoenix is set up to serve endpoints and that the SQL sandbox is enabled:
# config/test.exs
config :your_app, YourApplication.Endpoint,
server: true
config :your_app, :sql_sandbox, true
Then in your test_helper.exs
you can provide some configuration to Wallaby. At minimum, you need to specify a :base_url
, so Wallaby knows how to resolve relative paths.
# test/test_helper.exs
Application.put_env(:wallaby, :base_url, YourApplication.Endpoint.url)
Wallaby requires PhantomJS. You can install PhantomJS through NPM or your package manager of choice:
$ npm install -g phantomjs-prebuilt
Wallaby will use whatever PhantomJS you have installed in your path. If you need to specify a specific PhantomJS you can pass the path in the configuration:
config :wallaby, phantomjs: "some/path/to/phantomjs"
You can also pass arguments to PhantomJS through the phantomjs_args
config setting, e.g.:
config :wallaby, phantomjs_args: "--webdriver-logfile=phantomjs.log"
It's easiest to add Wallaby to your test suite by creating a new case template:
defmodule YourApp.FeatureCase do
use ExUnit.CaseTemplate
using do
quote do
use Wallaby.DSL
alias YourApp.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import YourApp.Router.Helpers
end
end
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(YourApp.Repo)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(YourApp.Repo, {:shared, self()})
end
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, self())
{:ok, session} = Wallaby.start_session(metadata: metadata)
{:ok, session: session}
end
end
Then you can write tests like so:
defmodule YourApp.UserListTest do
use YourApp.FeatureCase, async: true
test "users have names", %{session: session} do
first_employee =
session
|> visit("/users")
|> find(Query.css(".dashboard"))
|> all(Query.css(".user"))
|> List.first
|> find(Query.css(".user-name"))
|> Element.text
assert first_employee == "Chris"
end
end
The full documentation for the DSL is in the official documentation.
You can navigate directly to pages with visit
:
visit(session, "/page.html")
visit(session, user_path(Endpoint, :index, 17))
It's also possible to click links directly:
click(session, Query.link("Page 1"))
Queries are used to find and interact with elements through a browser (see Wallaby.Browser
). You can create queries like so:
Query.css(".some-css")
Query.xpath(".//input")
Query.button("Some Button")
These queries can then be used to find or interact with an element
@user_form Query.css(".user-form")
@name_field Query.text_field("Name")
@email_field Query.text_field("Email")
@save_button Query.button("Save")
find(page, @user_form, fn(form) ->
form
|> fill_in(@name_field, with: "Chris")
|> fill_in(@email_field, with: "[email protected]")
|> click(@save_button)
end)
If a callback is passed to find
then the find
will return the parent and the callback can be used to interact with the element.
By default, Wallaby will block until it can find the matching element. This is used to keep asynchronous tests in sync (as discussed below).
Nodes can also be found by their inner text:
# <div class="user">
# <span class="name">
# Chris K
# </span>
# </div>
find(page, Query.css(".user", text: "Chris K"))
There are a few ways to interact with form elements on a page:
fill_in(session, Query.text_field("First Name"), with: "Chris")
clear(session, Query.text_field("last_name"))
click(session, Query.option("Some option"))
click(session, Query.button("Some Button"))
It's possible to interact with the window and take screenshots:
resize_window(session, 100, 100)
window_size(session)
take_screenshot(session)
All screenshots are saved to a screenshots
directory in the directory that the tests were run in.
If you want to customize the screenshot directory you can pass it as a config value:
# config/test.exs
config :wallaby, screenshot_dir: "/file/path"
# test_helper.exs
Application.put_env(:wallaby, :screenshot_dir, "/file/path")
You can automatically take screenshots on an error:
# config/test.exs
config :wallaby, screenshot_on_failure: true
# test_helper.exs
Application.put_env(:wallaby, :screenshot_on_failure, true)
It can be difficult to test asynchronous JavaScript code. You may try to interact with an element that isn't visible on the page. Wallaby's finders try to help mitigate this problem by blocking until the element becomes visible. You can use this strategy by writing tests in this way:
session
|> click(Query.button("Some Async Button"))
|> find(Query.css(".async-result"))
Wallaby provides several ways to interact with JavaScript dialogs such as window.alert
, window.confirm
and window.prompt
. To accept/dismiss all dialogs in the current session you can use accept_dialogs
and dismiss_dialogs
. The default behavior is equivalent to using dismiss_dialogs
.
For more fine-grained control over individual dialogs, you can use one of the following functions:
- For
window.alert
useaccept_alert/2
- For
window.confirm
useaccept_confirm/2
ordismiss_confirm/2
- For
window.prompt
useaccept_prompt/2-3
ordismiss_prompt/2
All of these take a function as last parameter, which must include the necessary interactions to trigger the dialog. For example:
alert_message = accept_alert session, fn(session) ->
click(session, Query.link("Trigger alert"))
end
To emulate user input for a prompt, accept_prompt
takes an optional parameter:
prompt_message = accept_prompt session, [with: "User input"], fn(session) ->
click(session, Query.link("Trigger prompt"))
end
Wallaby captures both JavaScript logs and errors. Any uncaught exceptions in JavaScript will be re-thrown in Elixir. This can be disabled by specifying js_errors: false
in your Wallaby config.
JavaScript logs are written to :stdio by default. This can be changed to any IO device by setting the :js_logger
option in your Wallaby config. For instance if you want to write all JavaScript console logs to a file you could do something like this:
{:ok, file} = File.open("browser_logs.log", [:write])
Application.put_env(:wallaby, :js_logger, file)
Logging can be disabled by setting :js_logger
to nil
.
Wallaby is a community project. PRs and Issues are greatly welcome.
To get started and setup the project, make sure you've got Elixir 1.3+ installed and then:
$ mix deps.get
$ npm install -g phantomjs-prebuilt # unless you've already got PhantomJS installed
$ mix test # Make sure the tests pass!