Excessibility
Accessibility Snapshot Testing for Elixir + Phoenix
Excessibility helps you test your Phoenix apps for accessibility (WCAG compliance) by taking HTML snapshots during tests and running them through Pa11y.
Why Excessibility?
- Keep accessibility in your existing test feedback loop. Snapshots are captured inside ExUnit, Wallaby, and LiveView tests, so regressions surface together with your functional failures.
- Ship safer refactors. Explicit baseline locking and comparison lets reviewers see exactly what changed and approve intentionally.
- Debug CI-only failures quickly. Pa11y output points to the failing snapshot, and the saved artifacts make it easy to reproduce locally.
How It Works
- During tests, call
html_snapshot(conn)to capture HTML from your Phoenix responses, LiveViews, or Wallaby sessions - After tests, run
mix excessibilityto check all snapshots with Pa11y for WCAG violations - Lock baselines with
mix excessibility.baselinewhen snapshots represent a known-good state - Compare changes with
mix excessibility.compareto review what changed and approve/reject - In CI, Pa11y reports accessibility violations alongside your test failures
Features
- Snapshot HTML from
Plug.Conn,Wallaby.Session,Phoenix.LiveViewTest.View, andPhoenix.LiveViewTest.Element - Explicit baseline locking and comparison workflow
- Interactive good/bad approval when comparing snapshots
- Optional PNG screenshots via ChromicPDF
- Mockable system/browser calls for CI
- Pa11y configuration with sensible LiveView defaults
Installation
Add to mix.exs:
def deps do [ {:excessibility, "~> 0.5", only: [:dev, :test]} ] end
Fetch dependencies and run the installer:
mix deps.get mix excessibility.install
Apps with authentication: If your app requires login to access most pages, specify a public route for extracting <head> content:
mix excessibility.install --head-render-path /login
The installer will:
- Add configuration to
config/test.exs - Create a
pa11y.jsonwith sensible defaults for Phoenix/LiveView - Install Pa11y via npm in your assets directory
Quick Start
-
Configure the endpoint and helper modules in
config/test.exs. The installer does this automatically, or add manually:config :excessibility, endpoint: MyAppWeb.Endpoint, head_render_path: "/", # use "/login" for apps with auth system_mod: Excessibility.System, browser_mod: Wallaby.Browser, live_view_mod: Excessibility.LiveView
-
Add
use Excessibilityin tests where you want snapshots:defmodule MyAppWeb.PageControllerTest do use MyAppWeb.ConnCase, async: true use Excessibility test "renders home page", %{conn: conn} do conn = get(conn, "/") html_snapshot(conn, screenshot?: true) assert html_response(conn, 200) =~ "Welcome!" end end
-
Typical workflow:
# Write tests with html_snapshot calls, then: mix test # Generates snapshots in test/excessibility/ mix excessibility # Runs Pa11y against snapshots, reports violations # Lock current snapshots as known-good baseline mix excessibility.baseline # After making UI changes, run tests again, then compare mix test mix excessibility.compare # Review diffs, choose good (baseline) or bad (new)
Usage
use Excessibility html_snapshot(conn, name: "homepage.html", screenshot?: true )
The html_snapshot/2 macro works with:
Plug.ConnWallaby.SessionPhoenix.LiveViewTest.ViewPhoenix.LiveViewTest.Element
It returns the source unchanged, so you can use it in pipelines.
Options
| Option | Type | Default | Description |
|---|---|---|---|
:name |
string |
auto-generated | Custom filename (e.g., "login_form.html"). Default is ModuleName_LineNumber.html |
:screenshot? |
boolean |
false |
Generate PNG screenshots (requires ChromicPDF) |
:open_browser? |
boolean |
false |
Open the snapshot in your browser after writing |
:cleanup? |
boolean |
false |
Delete existing snapshots for the current test module before writing |
Baseline Workflow
Snapshots are saved to test/excessibility/html_snapshots/ and baselines live in test/excessibility/baseline/.
Setting a baseline:
mix excessibility.baseline
This copies all current snapshots to the baseline directory. Run this when your snapshots represent a known-good, accessible state.
Comparing against baseline:
mix excessibility.compare
For each snapshot that differs from its baseline:
- Diff files are created —
.good.html(baseline) and.bad.html(new) - Both open in your browser for visual comparison
- You choose which to keep — "good" to reject changes, "bad" to accept as new baseline
- Diff files are cleaned up after resolution
Batch options:
mix excessibility.compare --keep good # Keep all baselines (reject all changes) mix excessibility.compare --keep bad # Accept all new versions as baseline
Configuration
All configuration goes in test/test_helper.exs or config/test.exs:
| Config Key | Required | Default | Description |
|---|---|---|---|
:endpoint |
Yes | — | Your Phoenix endpoint module (e.g., MyAppWeb.Endpoint) |
:system_mod |
No | Excessibility.System |
Module for system commands (mockable) |
:browser_mod |
No | Wallaby.Browser |
Module for browser interactions |
:live_view_mod |
No | Excessibility.LiveView |
Module for LiveView rendering |
:excessibility_output_path |
No | "test/excessibility" |
Base directory for snapshots |
:pa11y_path |
No | auto-detected | Path to Pa11y executable |
:pa11y_config |
No | "pa11y.json" |
Path to Pa11y config file |
:head_render_path |
No | "/" |
Route used for rendering <head> content |
Example:
# test/test_helper.exs Application.put_env(:excessibility, :endpoint, MyAppWeb.Endpoint) Application.put_env(:excessibility, :system_mod, Excessibility.System) Application.put_env(:excessibility, :browser_mod, Wallaby.Browser) Application.put_env(:excessibility, :live_view_mod, Excessibility.LiveView) Application.put_env(:excessibility, :excessibility_output_path, "test/accessibility") ExUnit.start()
Pa11y Configuration
The installer creates a pa11y.json in your project root with sensible defaults for Phoenix/LiveView:
{
"ignore": [
"WCAG2AA.Principle3.Guideline3_2.3_2_2.H32.2"
]
}The ignored rule (H32.2) is "Form does not contain a submit button" — a common false positive for LiveView forms that use phx-submit without traditional submit buttons.
Add additional rules to ignore as needed for your project:
{
"ignore": [
"WCAG2AA.Principle3.Guideline3_2.3_2_2.H32.2",
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
]
}Screenshots
To enable PNG screenshots, start ChromicPDF in your test helper:
# test/test_helper.exs {:ok, _} = ChromicPDF.start_link(name: ChromicPDF) ExUnit.start()
Then use screenshot?: true in your snapshots:
html_snapshot(conn, screenshot?: true)
Screenshots are saved alongside HTML files with .png extension.
Mix Tasks
| Task | Description |
|---|---|
mix excessibility.install |
Configure config/test.exs, create pa11y.json, install Pa11y via npm |
mix excessibility |
Run Pa11y against all generated snapshots |
mix excessibility.baseline |
Lock current snapshots as baseline |
mix excessibility.compare |
Compare snapshots against baseline, resolve diffs interactively |
mix excessibility.compare --keep good |
Keep all baseline versions (reject changes) |
mix excessibility.compare --keep bad |
Accept all new versions as baseline |
CI and Non-Interactive Environments
For CI or headless environments where you don't want interactive prompts or browser opens, mock the system module:
# test/test_helper.exs Mox.defmock(Excessibility.SystemMock, for: Excessibility.SystemBehaviour) Application.put_env(:excessibility, :system_mod, Excessibility.SystemMock)
Then stub in your tests:
import Mox setup :verify_on_exit! test "snapshot without browser open", %{conn: conn} do Excessibility.SystemMock |> stub(:open_with_system_cmd, fn _path -> :ok end) conn = get(conn, "/") html_snapshot(conn, open_browser?: true) # Won't actually open end
File Structure
test/
└── excessibility/
├── html_snapshots/ # Current test snapshots
│ ├── MyApp_PageTest_42.html
│ └── MyApp_PageTest_42.png # (if screenshot?: true)
└── baseline/ # Locked baselines (via mix excessibility.baseline)
└── MyApp_PageTest_42.html
During mix excessibility.compare, temporary .good.html and .bad.html files are created for diffing, then cleaned up after resolution.
License
MIT © Andrew Moore