Fast Phoenix feature tests with real-browser confidence.

Single API to feature test
- LiveViews
- Controllers (static/dead views)
- In Browser
Vertically integrated: It's like PhoenixTest + Playwright. Or Capybara + Cuprite. Why you ask?
- You can easily switch from non-browser tests (fast!) to browser tests when you add JS hooks.
- We can guarantee correctness: Phoenix drivers are tested against real browser behaviour.
30-Second Start
# mix.exs
{:cerberus, "~> 0.1"}sh> MIX_ENV=test mix cerberus.install.chrome
import Cerberus
session
|> visit("/auth/static/users/log_in")
|> fill_in(~l"Email"l, "frodo@example.com")
|> assert_value(~l"Email"l, "frodo@example.com")
|> fill_in(~l"Password"l, "shire-secret")
|> submit(~l"Log in"e)
|> assert_has(~l"Signed in as: frodo@example.com"e)
import Cerberus.Browser
session(:browser, headless: false, slow_mo: 500) # open chrome
|> visit("/live/counter")
|> evaluate_js("prompt('Hey!')")
|> screenshot(full_page: true, open: true)For progressive, step-by-step examples (scopes, forms, tabs, browser extensions), see Getting Started.
Helpful errors
When an action/assertion misses, Cerberus includes likely alternatives.
session()
|> visit("/search")
|> submit(~l"Definitely Missing Submit"e)submit failed: no submit button matched locator
locator: %Cerberus.Locator{kind: :text, value: "Definitely Missing Submit", opts: [exact: true]}
...
possible candidates:
- "Run Search"
- "Run Nested Search"Locators
A locator is the way Cerberus finds elements or text in the UI.
Use composable locator functions when matching needs structure (label, role, text, filter, and_, closest, ...).
Use ~l sigil shorthand for common one-liners:
~l"Save"means exact text match by default~l"Save"imeans inexact text match~l"button:Save"rmeans role + accessible name- text-like matches normalize whitespace by default (
normalize_ws: true), including NBSP characters - set
normalize_ws: falsewhen you need exact raw whitespace matching
Use testid(...) when text/role is ambiguous, and CSS for structural targeting only.
For locator forms and advanced composition (~l modifiers, and_, or_, not_, filter, closest), see:
Debugging
session()
|> visit("/articles")
|> open_browser() # 1) human: open static HTML snapshot in browser
|> render_html(&IO.inspect(LazyHTML.query(&1, "h1"))) # 2) AI: inspect static HTML snapshot
session(:browser, show_browser: true, slow_mo: 500) # 3) human: watch live interaction in browser
|> visit("/articles")
|> evaluate_js("document.body.dataset.cerberus = 'ready'", fn _ -> :ok end)
png =
session(:browser)
|> visit("/articles")
|> screenshot(path: "tmp/page.png", return_result: true) # raw PNG bytesBrowser Tests
Start in Phoenix mode (static/live) for fast feedback, then switch to browser mode when you add JS-dependent behavior (custom snippets, dialogs, drag/drop, popup flows). In many tests this is just changing session() to session(:browser).
visit/2 waits for post-navigation browser readiness and auto-detects LiveView roots ([data-phx-session]), only waiting for phx-connected when a LiveView is present. Other browser actions rely on browser actionability and on the next action/assertion to wait for whatever state it needs.
Install Chrome with:
MIX_ENV=test mix cerberus.install.chrome
That task is simple to run in CI setup steps too.
Most tests only need session(:browser); deeper runtime/config details are documented in Browser Support Policy.
Performance
- Non-browser Phoenix mode is the fast lane for most feature tests.
- Browser assertions/path checks run polling in browser JS and include bounded retry handling for navigation/context-reset races.
- Browser-mode throughput is in the same class as Playwright-style real-browser E2E (both pay real browser/runtime costs), while Cerberus keeps one API across both lanes.