Session and Driver Selection

GoalCall
Phoenix mode (auto static/live switching)session() or session(:phoenix)
Real browser behaviorsession(:browser)
Public browser entrypointsession(:browser)
Default project lane policyChrome-first (CI and regular local runs)
Unified default timeoutStatic 0ms, live/browser 500ms
Per-session timeout overridesession(timeout_ms: 300) or session(:browser, timeout_ms: 300)
Browser ready timeout defaultsession(:browser, ready_timeout_ms: 2200)
Per-driver timeout configconfig :cerberus, :live, timeout_ms: 700
Global headed modeconfig :cerberus, :browser, headless: false
Global slow motionconfig :cerberus, :browser, slow_mo: 120
Global remote runtimeconfig :cerberus, :browser, webdriver_url: "http://127.0.0.1:4444"
Global screenshot defaultsconfig :cerberus, :browser, screenshot_full_page: false, screenshot_artifact_dir: "tmp/screenshots"

Core Navigation and Assertions

TaskExample
Visit pagevisit(session, "/articles")
Click link/buttonclick(session, ~l"link:Counter"r)
Fill inputfill_in(session, ~l"Search term"l, "Aragorn")
Select optionselect(session, ~l"Race"l, option: ~l"Elf"e)
Choose radiochoose(session, ~l"Email Choice"l)
Check checkboxcheck(session, ~l"Accept Terms"l)
Uncheck checkboxuncheck(session, ~l"Receive updates"l)
Upload fileupload(session, ~l"Avatar"l, "/tmp/avatar.jpg")
Submit formsubmit(session, ~l"button:Run Search"r)
Bypass browser actionability checksclick(session, ~l"button:Hidden Action"r, force: true)
Assert text presentassert_has(session, ~l"Articles"e)
Assert text absentrefute_has(session, ~l"Error"e)
Assert checked stateassert_checked(session, ~l"Mail Choice"l)
Refute checked staterefute_checked(session, ~l"Email Choice"l)
Assert disabled stateassert_disabled(session, ~l"Disabled textaread"l)
Refute disabled staterefute_disabled(session, ~l"Notes"l)
Assert readonly stateassert_readonly(session, ~l"Readonly notes"l)
Refute readonly staterefute_readonly(session, ~l"Notes"l)
Assert scoped textassert_has(session, ~l"#secondary-panel"c, ~l"Status: secondary"e)
Refute scoped textrefute_has(session, ~l"#secondary-panel"c, ~l"Status: primary"e)
Assert path/queryassert_path(session, "/search/results", query: %{q: "Aragorn"}, timeout: 500)
Scope to subtreewithin(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_has and path assertions use in-browser wait loops.
  • Cerberus adds bounded transient eval retries for navigation/context-reset races.

Multi-Session Operations

TaskExample
New user (isolated state)session() / session(:browser)
New tab (shared user state)open_tab(session)
Switch active tab/sessionswitch_tab(session, other_session)
Close current tabclose_tab(session)

Locators

Default strategy:

  • prefer user-facing locators first (label text, role + name, visible text)
  • use testid when text is ambiguous or intentionally hidden
  • use CSS as a last resort for structure-only targeting

Common Phoenix/LiveView cases

GoalPreferred locatorExample
Fill a text inputlabel textfill_in(session, ~l"Email"l, "alice@example.com")
Click a buttonrole + nameclick(session, ~l"button:Save"r)
Click a linkrole + nameclick(session, ~l"link:Billing"r)
Assert rendered contentvisible textassert_has(session, ~l"Settings saved"e)
Operate inside repeated UIscope + same locatorswithin(session, ~l"#shipping-address"c, fn s -> fill_in(s, ~l"City"l, "Berlin") end)
Disambiguate duplicate controlstestidclick(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"..."l
  • testid("...")
  • 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

LocatorMeaning
~l"Save"exact text (default)
~l"Save"eexact text
~l"Save"iinexact text
~l"Email"lfield label locator (<label>, aria-labelledby, or aria-label)
~l"button:Save"rrole-style locator
~l"button[type='submit']"ccss locator
~l"save-button"ttestid locator (exact: true default)
~l"button:Save"rerole + exact

Rules:

  • at most one kind modifier (r, c, l, or t)
  • e and i are mutually exclusive
  • r requires ROLE: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: false to 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).

TaskExample
ScreenshotBrowser.screenshot(session, path: "tmp/page.png")
Screenshot binary resultpng = Browser.screenshot(session, path: "tmp/page.png", return_result: true)
Screenshot + open viewerBrowser.screenshot(session, path: "tmp/page.png", open: true)
Type keysBrowser.type(session, css("#input"), "hello")
Press keyBrowser.press(session, css("#input"), "Enter")
Drag and dropBrowser.drag(session, "#drag-source", "#drop-target")
Dialog assert + dismissBrowser.assert_dialog(session, ~l"Delete item?"e)
Dialog assert + confirmBrowser.assert_dialog(session, ~l"Delete item?"e, accept: true)
Popup capturesession |> 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 fallbacksession(: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 lookupBrowser.cookie(session, "_my_cookie")
Cookie callbackBrowser.cookie(session, "_my_cookie", fn cookie -> assert cookie end)
Bulk cookie addBrowser.add_cookies(session, [[name: "feature", value: "on"]])
Clear cookiesBrowser.clear_cookies(session)
Seed Phoenix sessionBrowser.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)