Starting Browser Testing for Phoenix LiveView with Wallaby
Browser testing can be a powerful way to build confidence in your codebase, and it's easy to get started in a Phoenix app with Wallaby.
I gave a conference talk about this at The Big Elixir 2022 with a few more examples. If you just want to dive into the code, it can be found here.
What is Wallaby?
Wallaby is a browser testing tool written in Elixir that allows you to drive a browser, and write browser-based tests all without leaving the Elixir ecosystem. So you have access to all your favorite tools like ExUnit.
Setting Up
Install Wallaby
Add the dependency to your mix.exs:
defp deps do
...
{:wallaby, "~> 0.29.0", runtime: false, only: :test}
end
Install Chromedriver
How you install the chromedriver is going to depend on your preferences. On MacOS you can install it with brew
but the latest security updates make that a little more painful and less cross-platform friendly in the event that cross-platform matters to you.
brew install --cask chromedriver && \ xattr -d com.apple.quarantine /usr/local/Caskroom/chromedriver/$VERSION/chromedriver
You can also install the binary directly from https://sites.google.com/chromium.org/driver/.
Finally, the easiest cross-platform option that I've found is just installing it via npm
.
npm install chromedriver --save-dev --prefix assets
Update the endpoint
You need to update the endpoint.ex
to leverage Ecto's SQL sandbox, which allows your concurrent/async tests to be isolated at the database layer.
You also need to modify the socket connect_info
to include :user_agent
this means that the user agent information is passed along to the socket. The user agent is how Wallaby links up which isolated sandbox database process should be connected with the particular browser test session.
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
if Application.get_env(:my_app, :sql_sandbox) do
plug(Phoenix.Ecto.SQL.Sandbox)
end
...
socket("/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [:user_agent, session: @session_options]]
)
...
end
Update the test helper
We need to ensure Wallaby is started as part of running your tests:
{:ok, _} = Application.ensure_all_started(:wallaby)
...
Update the test config
You now want to configure your application in test to use the Ecto SQL sandbox, and then you need to configure Wallaby.
import Config
...
config :my_app, sql_sandbox: true
config :wallaby,
base_url: MyAppWeb.Endpoint.url(),
otp_app: :my_app,
screenshot_on_failure: true,
chromedriver: [
path: "assets/node_modules/chromedriver/bin/chromedriver", # point to your chromedriver path
headless: true # change to false if you want to see the browser in action
]
A few of the options that you may want to consider modifying include:
screenshot_on_failure
- this option ensures that the . You can also configure the path to where the screenshots ought to be dumped.chromedriver
- includes thepath
to find the chromedriver binary, as well asheadless
mode, which determines if you'll see the actual browser bootup and take the actions you've given it, or if it will run in the background.
Add the on_mount hook
If you're using Phoenix LiveView you'll also need to add an on_mount
hook, which leverages the mount
lifecycle hook to ensure that on each LiveView mount the browser and test instance are correctly wired up together.
Note that this assumes Phoenix LiveView version 0.17.7. Things may have changed for you.
Below is an example hook implementation:
defmodule MyAppWeb.Hooks.AllowEctoSandbox do
import Phoenix.LiveView
def on_mount(:default, _params, _session, socket) do
allow_ecto_sandbox(socket)
{:cont, socket}
end
defp allow_ecto_sandbox(socket) do
if Application.get_env(:testing_live_view_wallaby, :sql_sandbox) do
%{assigns: %{phoenix_ecto_sandbox: metadata}} =
assign_new(socket, :phoenix_ecto_sandbox, fn ->
if connected?(socket), do: get_connect_info(socket, :user_agent)
end)
Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
end
end
end
You'll then need to update the router to use this on_mount
hook for each of your LiveView sessions with the live_session
helper.
defmodule MyAppWeb.Router do
...
live_session :default, on_mount: TestingLiveViewWallabyWeb.Hooks.AllowEctoSandbox do
...
end
end
Setup a FeatureCase
This one is optional, but I've found it useful for developer ergonomics when you're building with Wallaby. It sets up an ExUnit case similar to the generated ConnCase
, but geared specifically for Wallaby.
defmodule MyAppWeb.FeatureCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a full browser.
If the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use MyAppWeb.FeatureCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
use Wallaby.Feature
import MyAppWeb.FeatureCase
import Wallaby.Query
alias MyAppWeb.Router.Helpers, as: Routes
@moduletag :e2e
@endpoint MyAppWeb.Endpoint
setup _ do
on_exit(fn -> Application.put_env(:wallaby, :js_logger, :stdio) end)
end
end
end
def enable_latency_sim(session, latency) do
Application.put_env(:wallaby, :js_logger, nil)
Wallaby.Browser.execute_script(session, "liveSocket.enableLatencySim(#{latency})")
end
def disable_latency_sim(session) do
Wallaby.Browser.execute_script(session, "liveSocket.disableLatencySim()")
end
end
Testing a simple form interaction
You can see the full example on GitHub to enable this test to go green. But here is a snippet:
describe "live index when creating a question" do
feature "users should be able to submit the form and create a question", %{session: session} do
question_text = "How do I test simple things with Wallaby?"
session
|> visit("/questions")
|> click(link("New Question"))
|> fill_in(text_field("Text"), with: question_text)
|> click(button("Save"))
|> assert_has(css(".alert", text: "Question created successfully"))
|> assert_has(css("#questions > tr > td", text: question_text))
end
end
Simulate Latency
Finally, thanks to JavaScript helpers provided by LiveView, we can also simulate high levels of latency in the user's network:
describe "simulating latency" do
feature "should ensure the saving is shown", %{session: session} do
html =
session
|> visit("/questions")
|> click(link("New Question"))
|> fill_in(text_field("Text"), with: "Latency isn't fun, but should be accounted for")
|> enable_latency_sim(2000)
|> click(button("Save"))
|> find(css("#question-form > div > button"))
|> Wallaby.Element.attr("innerHTML")
assert html == "Saving..."
end
end
Conclusion
Now you should be able to get started testing your Phoenix application using Wallaby.