Cerberus is built around one session-first API and two user-facing modes (:phoenix, :browser).
Internally, Phoenix mode dispatches between static and live execution backends.
Layering
flowchart LR
A["Public API<br/>Cerberus"] --> B["Assertions + Locator Normalization"]
B --> C["Driver Dispatch"]
C --> D["Static Driver<br/>ConnTest"]
C --> E["Live Driver<br/>LiveViewTest"]
C --> F["Browser Driver<br/>WebDriver BiDi"]
D --> G["HTML Semantics<br/>Cerberus.Html"]
E --> G
E --> H["LiveView Semantics<br/>Cerberus.Phoenix.LiveViewHTML"]
F --> I["Browser Runtime<br/>Shared worker + per-test context"]Why this shape?
CerberusAPI stays consistent across drivers.- Shared semantics reduce drift in assertions and locator behavior.
- Browser mode serves as an oracle for HTML/DOM behavior when needed.
Semantic Boundaries
- HTML semantics layer: platform behavior (labels, form ownership, control defaults, text extraction).
- LiveView semantics layer: framework behavior (
phx-click,phx-change,phx-submit,phx-trigger-action).
Info
This split keeps HTML rules testable without Phoenix internals and isolates LiveView-specific quirks to one layer.
Waiting Model
- Static stays one-shot. It inspects the current document and path snapshot and returns immediately.
- Live owns its own waiting. Actions and assertions wait on LiveView progress and follow redirects or static transitions inside the live driver.
- Browser owns its own waiting. Actions and assertions wait natively in the browser driver and browser-side JS.
- Cerberus does not use a shared timeout loop across static, live, and browser. The public layer computes timeout values once and passes them down to the active driver.
Driver Module Map
Driver-first organization is the primary code layout rule:
- Static driver core:
lib/cerberus/driver/static.exandlib/cerberus/driver/static/form_data.ex. - Live driver core:
lib/cerberus/driver/live.exandlib/cerberus/driver/live/form_data.ex. - Browser driver core:
lib/cerberus/driver/browser.exfor orchestration.lib/cerberus/driver/browser/config.exfor browser option normalization.lib/cerberus/driver/browser/expressions.exfor browser-side JS expression builders.lib/cerberus/driver/browser/runtime.ex,lib/cerberus/driver/browser/bidi.ex, and browser socket/context supervisors for BiDi lifecycle.
Cross-driver concern modules remain separate:
lib/cerberus/html/html.exfor HTML-spec behavior.lib/cerberus/phoenix/*for Phoenix/LiveView-specific behavior.
Multi-User / Multi-Tab Semantics
open_tab/1creates another tab in the same user state in all drivers.- Start a new isolated user by creating a new session (
session()orsession(:browser)). - Browser maps this to separate browsing contexts inside one user context.
- Static/live map tabs to separate
Plug.Connsessions with shared cookie semantics.
Browser Overrides and Isolation
- Browser runtime process and BiDi socket are shared for efficiency.
- Each browser session (
session(:browser, ...)) creates a dedicated browser user context. - Per-session browser overrides (
browser: [viewport: ..., user_agent: ..., init_script: ...]) apply only to that session's user context. - Result: test/module overrides do not require dedicated browser processes unless you need different runtime-level launch settings.
Test Organization (Maintainer)
Integration contract tests are located under test/cerberus and use Cerberus.* module names.
Driver internals stay in driver-focused test paths such as test/cerberus/driver/browser.
Cross-driver integration tests use plain ExUnit loops rather than a custom harness:
for driver <- [:phoenix, :browser] do
test "works across drivers (#{driver})" do
session(unquote(driver))
|> visit("/page")
|> assert_has(text("ok"))
end
endEvent Waiter Policy
For browser event assertions (assert_download/3, assert_dialog/3, popup capture), waiter registration and blocking must happen in the owning process (BrowsingContextProcess or UserContextProcess), not in external polling loops in Extensions.
- Why: external polling can miss edge timing between "check state" and "sleep".
- Rule: waiters first check current process state/history, then register a timed waiter if not yet satisfied.
- Event handlers resolve waiters directly from process state updates; timeout replies include observed events for diagnostics.
Escape Hatches
unwrap/2: direct access to underlying driver primitives when needed. Browser unwrap callbacks receiveCerberus.Browser.Native(opaque handle), not a raw internals map.Cerberus.Browser.*: browser-only low-level APIs for keyboard, drag, dialogs, JS evaluation, and cookies.
Warning
Keep escape hatches localized. Prefer shared Cerberus operations in conformance scenarios so switching between browser and non-browser modes stays cheap.
Guardrails for unwrap/2:
- Treat browser native handles as unstable implementation details; avoid asserting on raw process-state internals.
- Prefer
Cerberus.Browser.*for browser behavior that already has a public API. - Keep unwrap usage isolated to one helper per test module where possible.