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.
- 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. Baseline comparison saves
.good/.bad.html(plus screenshots when enabled) so reviewers can 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.
- 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 - When HTML changes, snapshots are diffed against approved baselines — review and approve changes with
mix excessibility.approve - In CI, Pa11y reports accessibility violations alongside your test failures
- Snapshot HTML from
Plug.Conn,Wallaby.Session,Phoenix.LiveViewTest.View, andPhoenix.LiveViewTest.Element - Automatically diff against saved baselines
- Interactive approval (good/bad) when snapshots change
- Optional PNG screenshots via ChromicPDF
- Mockable system/browser calls for CI
- Pa11y configuration with sensible LiveView defaults
Add to mix.exs:
def deps do
[
{:excessibility, "~> 0.5", only: [:dev, :test]}
]
endFetch dependencies and run the installer:
mix deps.get
mix igniter.install excessibilityThe installer will:
- Add configuration to
test/test_helper.exs - Create a
pa11y.jsonwith sensible defaults for Phoenix/LiveView - Install Pa11y via npm in your assets directory
-
Configure the endpoint and helper modules in
test/test_helper.exs. The installer does this automatically, or add manually: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)
-
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, prompt_on_diff: false, 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/ # Check accessibility mix excessibility # Runs Pa11y against snapshots, reports violations # When snapshots change (after updating your UI) mix excessibility.approve # Review and approve/reject changes
use Excessibility
html_snapshot(conn,
name: "homepage.html",
prompt_on_diff: false,
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.
| Option | Type | Default | Description |
|---|---|---|---|
:name |
string |
auto-generated | Custom filename (e.g., "login_form.html"). Default is ModuleName_LineNumber.html |
:prompt_on_diff |
boolean |
true |
Interactively choose which snapshot to keep when diff detected |
:tag_on_diff |
boolean |
true |
Save diffs as .bad.html and .good.html files |
: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 |
Snapshots are saved to test/excessibility/html_snapshots/ and baselines live in test/excessibility/baseline/.
When a snapshot differs from its baseline:
-
Diff files are created:
.good.html— the current approved baseline.bad.html— the new snapshot from your test
-
You choose which to keep:
- If
prompt_on_diff: true(default), both files open in your browser and you're prompted to keep "good" (baseline) or "bad" (new) - Choose "good" to reject the changes
- Choose "bad" to approve the changes as the new baseline
- If
-
Baseline is updated with your choice, and the
.good.html/.bad.htmlfiles are cleaned up
First run: If no baseline exists yet, the snapshot is automatically saved as the baseline.
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()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"
]
}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.
| Task | Description |
|---|---|
mix igniter.install excessibility |
Configure test helper, create pa11y.json, install Pa11y via npm |
mix excessibility |
Run Pa11y against all generated snapshots |
mix excessibility.approve |
Interactively approve pending diffs |
mix excessibility.approve --keep good |
Keep all baseline (good) versions |
mix excessibility.approve --keep bad |
Accept all new (bad) versions as baseline |
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
endtest/
└── excessibility/
├── html_snapshots/ # Current test snapshots
│ ├── MyApp_PageTest_42.html
│ ├── MyApp_PageTest_42.png
│ ├── MyApp_PageTest_42.bad.html
│ └── MyApp_PageTest_42.good.html
└── baseline/ # Approved baselines
└── MyApp_PageTest_42.html
MIT © Andrew Moore