Session and Driver Selection
| Goal | Call |
|---|---|
| Phoenix mode (auto static/live switching) | session() or session(:phoenix) |
| Real browser behavior | session(:browser) |
| Public browser entrypoint | session(:browser) |
| Default project lane policy | Chrome-first (CI and regular local runs) |
| Unified default timeout | Static 0ms, live/browser 500ms |
| Per-session timeout override | session(timeout_ms: 300) or session(:browser, timeout_ms: 300) |
| Browser ready timeout default | session(:browser, ready_timeout_ms: 2200) |
| Per-driver timeout config | config :cerberus, :live, timeout_ms: 700 |
| Global headed mode | config :cerberus, :browser, headless: false |
| Global slow motion | config :cerberus, :browser, slow_mo: 120 |
| Global remote runtime | config :cerberus, :browser, webdriver_url: "http://127.0.0.1:4444" |
| Global screenshot defaults | config :cerberus, :browser, screenshot_full_page: false, screenshot_artifact_dir: "tmp/screenshots" |
Core Navigation and Assertions
| Task | Example |
|---|---|
| Visit page | visit(session, "/articles") |
| Click link/button | click(session, ~l"link:Counter"r) |
| Fill input | fill_in(session, ~l"Search term"l, "Aragorn") |
| Select option | select(session, ~l"Race"l, option: ~l"Elf"e) |
| Choose radio | choose(session, ~l"Email Choice"l) |
| Check checkbox | check(session, ~l"Accept Terms"l) |
| Uncheck checkbox | uncheck(session, ~l"Receive updates"l) |
| Upload file | upload(session, ~l"Avatar"l, "/tmp/avatar.jpg") |
| Submit form | submit(session, ~l"button:Run Search"r) |
| Bypass browser actionability checks | click(session, ~l"button:Hidden Action"r, force: true) |
| Assert text present | assert_has(session, ~l"Articles"e) |
| Assert text absent | refute_has(session, ~l"Error"e) |
| Assert checked state | assert_checked(session, ~l"Mail Choice"l) |
| Refute checked state | refute_checked(session, ~l"Email Choice"l) |
| Assert disabled state | assert_disabled(session, ~l"Disabled textaread"l) |
| Refute disabled state | refute_disabled(session, ~l"Notes"l) |
| Assert readonly state | assert_readonly(session, ~l"Readonly notes"l) |
| Refute readonly state | refute_readonly(session, ~l"Notes"l) |
| Assert scoped text | assert_has(session, ~l"#secondary-panel"c, ~l"Status: secondary"e) |
| Refute scoped text | refute_has(session, ~l"#secondary-panel"c, ~l"Status: primary"e) |
| Assert path/query | assert_path(session, "/search/results", query: %{q: "Aragorn"}, timeout: 500) |
| Scope to subtree | within(session, ~l"#secondary-panel"c, fn s -> ... end) |
Actionability defaults:
- browser waits for matched controls to become enabled
- live retries briefly when a matched form control is still disabled after a LiveView update
- static fails immediately on disabled controls
Browser assertion execution model:
assert_has/refute_hasand path assertions use in-browser wait loops.- Cerberus adds bounded transient eval retries for navigation/context-reset races.
Multi-Session Operations
| Task | Example |
|---|---|
| New user (isolated state) | session() / session(:browser) |
| New tab (shared user state) | open_tab(session) |
| Switch active tab/session | switch_tab(session, other_session) |
| Close current tab | close_tab(session) |
Locators
Default strategy:
- prefer user-facing locators first (label text, role + name, visible text)
- use
testidwhen text is ambiguous or intentionally hidden - use CSS as a last resort for structure-only targeting
Common Phoenix/LiveView cases
| Goal | Preferred locator | Example |
|---|---|---|
| Fill a text input | label text | fill_in(session, ~l"Email"l, "alice@example.com") |
| Click a button | role + name | click(session, ~l"button:Save"r) |
| Click a link | role + name | click(session, ~l"link:Billing"r) |
| Assert rendered content | visible text | assert_has(session, ~l"Settings saved"e) |
| Operate inside repeated UI | scope + same locators | within(session, ~l"#shipping-address"c, fn s -> fill_in(s, ~l"City"l, "Berlin") end) |
| Disambiguate duplicate controls | testid | click(session, testid("apply-secondary-button")) |
Supported role aliases
- Click/assert roles:
button,menuitem,tab,link,heading,img - Form-control roles:
textbox,searchbox,combobox,listbox,spinbutton,checkbox,radio,switch
Helper constructors
text("...")~l"..."ltestid("...")css("...")role(:button | :link | :textbox | ..., name: "...")
Composition (advanced)
- scope chaining (descendant query):
css("#actions") |> role(:button, name: "Apply") - same-element AND intersection:
and_(role(:button, name: "Apply"), testid("apply-secondary-button")) - descendant requirement:
role(:button, name: "Apply") |> filter(has: testid("apply-secondary-marker")) - descendant exclusion:
role(:button, name: "Apply") |> filter(has_not: testid("apply-secondary-marker")) - visibility constraint:
role(:button, name: "Apply") |> filter(visible: true) - alternatives (OR):
or_(css("#primary"), css("#secondary")) - boolean algebra:
and_(role(:button, name: "Apply"), not_(testid("apply-secondary-button"))) - negated conjunction:
not_(and_(role(:button, name: "Apply"), testid("apply-secondary-button"))) - nearest ancestor scope:
closest(css(".fieldset"), from: ~l"Email"le)
Sigil ~l
| Locator | Meaning |
|---|---|
~l"Save" | exact text (default) |
~l"Save"e | exact text |
~l"Save"i | inexact text |
~l"Email"l | field label locator (<label>, aria-labelledby, or aria-label) |
~l"button:Save"r | role-style locator |
~l"button[type='submit']"c | css locator |
~l"save-button"t | testid locator (exact: true default) |
~l"button:Save"re | role + exact |
Rules:
- at most one kind modifier (
r,c,l, ort) eandiare mutually exclusiverrequiresROLE:NAME- regex values are supported for text-like locators and role names, but cannot be combined with
exact: true|false - text-like matching normalizes whitespace by default (
normalize_ws: true), including NBSP characters - use
normalize_ws: falseto require exact raw whitespace matching
Use role(..., name: ...) for supported accessible-name matching on buttons, links, headings, and similar non-form elements.
Browser-Only Extensions
Use Cerberus.Browser only with session(:browser).
| Task | Example |
|---|---|
| Screenshot | Browser.screenshot(session, path: "tmp/page.png") |
| Screenshot binary result | png = Browser.screenshot(session, path: "tmp/page.png", return_result: true) |
| Screenshot + open viewer | Browser.screenshot(session, path: "tmp/page.png", open: true) |
| Type keys | Browser.type(session, css("#input"), "hello") |
| Press key | Browser.press(session, css("#input"), "Enter") |
| Drag and drop | Browser.drag(session, "#drag-source", "#drop-target") |
| Dialog assert + dismiss | Browser.assert_dialog(session, ~l"Delete item?"e) |
| Dialog assert + confirm | Browser.assert_dialog(session, ~l"Delete item?"e, accept: true) |
| Popup capture | session |> Browser.with_popup(fn main -> click(main, ~l"button:Open Popup"r) end, fn _main, popup -> assert_path(popup, "/browser/popup/destination") end) |
| Popup same-tab fallback | session(:browser, browser: [popup_mode: :same_tab]) |> visit("/browser/popup/auto") |> assert_path("/browser/popup/destination", timeout: 1500) |
| Assert download (browser/static/live) | session |> click(~l"link:Download Report"r) |> assert_download("report.txt") |
| Evaluate JS (ignore result) | Browser.evaluate_js(session, "window.__cerberusMarker = 'ready'") |
| Evaluate JS (assert result) | Browser.evaluate_js(session, "(() => 42)()", fn value -> assert value == 42 end) |
| Cookie lookup | Browser.cookie(session, "_my_cookie") |
| Cookie callback | Browser.cookie(session, "_my_cookie", fn cookie -> assert cookie end) |
| Bulk cookie add | Browser.add_cookies(session, [[name: "feature", value: "on"]]) |
| Clear cookies | Browser.clear_cookies(session) |
| Seed Phoenix session | Browser.add_session_cookie(session, [value: %{user_id: user.id}], MyAppWeb.Endpoint.session_options()) |
Warning
Browser extension helpers intentionally raise on non-browser sessions to prevent silent semantic drift.
Mode Switching Pattern
-session()
+session(:browser)
|> visit("/articles")
|> assert_has(~l"Articles"e)