Skip to content

Repository files navigation

Excessibility

Hex.pmHex DocsCILicense: MITAsk DeepWiki

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

  1. During tests, call html_snapshot(conn) to capture HTML from your Phoenix responses, LiveViews, or Wallaby sessions
  2. After tests, run mix excessibility to check all snapshots with Pa11y for WCAG violations
  3. Lock baselines with mix excessibility.baseline when snapshots represent a known-good state
  4. Compare changes with mix excessibility.compare to review what changed and approve/reject
  5. In CI, Pa11y reports accessibility violations alongside your test failures

Features

  • Snapshot HTML from Plug.Conn, Wallaby.Session, Phoenix.LiveViewTest.View, and Phoenix.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

LLM Development Features

Excessibility includes powerful features for debugging Phoenix apps with AI assistance (Claude, Cursor, etc.).

Telemetry-Based Auto-Capture (Zero Code Changes!)

Debug any existing LiveView test with automatic snapshot capture - no test changes required:

# Your test - completely vanilla, zero Excessibility codetest"user interaction flow",%{conn: conn}do{:ok,view,_html}=live(conn,"/dashboard")view|>element("#button")|>render_click()view|>element("#form")|>render_submit(%{name: "Alice"})assertrender(view)=~"Welcome Alice"end

Debug it:

mix excessibility.debug test/my_test.exs

🚀 Rich Timeline Capture

mix excessibility.debug automatically enables telemetry capture, dramatically increasing event visibility:

  • Without telemetry: ~4 events (mount, handle_params only)
  • With telemetry: 10-20x more events including all render cycles

Example from real test:

  • Basic test: 4 events → 11 events (added 7 render events)
  • Complex test: Limited snapshots → 236 timeline events with rich analyzer insights

Render events enable powerful pattern detection:

  • 🔴 Memory leak detected (2.3x growth over render cycles)
  • ⚠️ 7 consecutive renders without user interaction
  • 🔴 Performance bottleneck (15ms render blocking)
  • ⚠️ Rapid state changes (potential infinite loop)

This happens automatically - no test changes needed!

Automatically captures:

  • LiveView mount events
  • All handle_event calls (clicks, submits, etc.)
  • All render cycles (form updates, state changes triggered by render_change, render_click, render_submit)
  • Real LiveView assigns at each step
  • Complete state timeline with memory tracking and performance metrics

Example captured snapshot:

<!--Excessibility Telemetry SnapshotTest: test user interaction flowSequence: 2Event: handle_event:submit_formTimestamp: 2026-01-25T10:30:12.345ZView Module: MyAppWeb.DashboardLiveAssigns: %{ current_user: %User{name: "Alice"}, form_data: %{name: "Alice"}, submitted: true}-->

Debug Command

The debug command outputs a comprehensive markdown report with:

  • Test results and error output
  • All captured snapshots with inline HTML
  • Event timeline showing state changes
  • Real LiveView assigns at each snapshot
  • Metadata (timestamps, event sequence, view modules)

The report is both human-readable and AI-parseable, perfect for pasting into Claude.

Available formats (all args pass through to mix test):

mix excessibility.debug test/my_test.exs # Markdown report (default)
mix excessibility.debug test/my_test.exs:42 # Run specific test
mix excessibility.debug --only live_view # Run tests with tag
mix excessibility.debug test/my_test.exs --format=json # Structured JSON
mix excessibility.debug test/my_test.exs --format=package # Directory with MANIFEST
mix excessibility.latest # Re-display last report

🔍 Telemetry Timeline Analysis

Automatically captures LiveView state throughout test execution and generates scannable timeline reports:

  • Smart Filtering - Removes Ecto metadata, Phoenix internals, and other noise
  • Diff Detection - Shows what changed between events
  • Multiple Formats - JSON for automation, Markdown for humans/AI
  • CLI Control - Override filtering with flags for deep debugging
mix excessibility.debug test/my_test.exs

See CLAUDE.md for detailed usage.

Telemetry Implementation

Excessibility hooks into Phoenix LiveView's built-in telemetry events:

  • [:phoenix, :live_view, :mount, :stop]
  • [:phoenix, :live_view, :handle_event, :stop]
  • [:phoenix, :live_view, :handle_params, :stop]
  • [:phoenix, :live_view, :render, :stop] - Captures all render cycles (form updates, state changes)

When you run mix excessibility.debug, it:

  1. Enables telemetry capture via environment variable
  2. Attaches telemetry handlers to LiveView events
  3. Runs your test
  4. Captures snapshots with real assigns from the LiveView process
  5. Generates a complete debug report

No test changes needed - it works with vanilla Phoenix LiveView tests!

Manual Capture Mode

For fine-grained control, you can also manually capture snapshots:

useExcessibility@tagcapture_snapshots: truetest"manual capture",%{conn: conn}do{:ok,view,_}=live(conn,"/")html_snapshot(view)# Manual snapshot with auto-tracked metadataview|>element("#btn")|>render_click()html_snapshot(view)# Another snapshotend

Claude Documentation

Create .claude_docs/excessibility.md to teach Claude how to use these debugging features:

mix excessibility.setup_claude_docs

MCP Server & Claude Code Skills

Excessibility includes an MCP (Model Context Protocol) server and Claude Code skills plugin for AI-assisted development.

MCP Server

The MCP server provides tools for AI assistants to run accessibility checks and debug LiveView state.

Available tools:

ToolSpeedDescription
check_routeFastRun Pa11y on a live route (requires app running)
explain_issueFastGet explanation and fix suggestions for a WCAG violation code
suggest_fixesFastGet Phoenix-specific code fixes for accessibility issues
generate_testFastGenerate test code with html_snapshot() calls for a route
list_analyzersFastList available timeline analyzers
get_timelineFastRead captured timeline showing LiveView state evolution
get_snapshotsFastList or read HTML snapshots captured during tests
analyze_timelineFastRun analyzers on captured timeline data
list_violationsFastList recent Pa11y violations from snapshots
e11y_checkSlowRun tests and/or Pa11y accessibility checks on HTML snapshots
e11y_debugSlowRun tests with telemetry capture - returns timeline for analysis

Recommended workflow:

  1. check_route - Quick accessibility check on running app
  2. explain_issue - Understand what violations mean
  3. generate_test - Create test with html_snapshot() calls
  4. e11y_debug - Run test to capture timeline
  5. analyze_timeline - Find performance issues

Automatic Setup:

MCP server support is configured automatically when you run the installer:

mix excessibility.install
mix deps.get

This creates .claude/mcp_servers.json with the excessibility MCP server configuration.

Use --no-mcp to skip MCP setup if you don't need AI assistant integration.

Manual Setup:

Configure Claude Code's mcp_servers.json:

{
"excessibility": {
"command": "mix",
"args": ["run", "--no-halt", "-e", "Excessibility.MCP.Server.start()"],
"cwd": "/path/to/your/project"
}
}

The MCP server is now available in Claude Code.

Claude Code Skills Plugin

Install the skills plugin for structured accessibility workflows:

claude plugins add /path/to/excessibility/priv/claude-plugin

Available skills:

SkillDescription
/e11y-tddTDD workflow with html_snapshot and Pa11y - sprinkle snapshots to see what's rendered, delete when done
/e11y-debugDebug workflow with timeline analysis - inspect state at each event, correlate with Pa11y failures
/e11y-fixReference guide for fixing Pa11y/WCAG errors with Phoenix-specific patterns

Example workflow:

/e11y-tdd
# Claude will guide you through:
# 1. EXPLORE - Add html_snapshot() calls to see what's rendered
# 2. RED - Write test with snapshot at key moment
# 3. GREEN - Implement feature, use snapshots to debug
# 4. CHECK - Run mix excessibility for Pa11y validation
# 5. CLEAN - Remove temporary snapshots

Installation

Add to mix.exs:

defdepsdo[{:excessibility,"~> 0.10",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.json with sensible defaults for Phoenix/LiveView
  • Install Pa11y via npm in your assets directory

Quick Start

  1. 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 authsystem_mod: Excessibility.System,browser_mod: Wallaby.Browser,live_view_mod: Excessibility.LiveView
  2. Add use Excessibility in tests where you want snapshots:

    defmoduleMyAppWeb.PageControllerTestdouseMyAppWeb.ConnCase,async: trueuseExcessibilitytest"renders home page",%{conn: conn}doconn=get(conn,"/")html_snapshot(conn,screenshot?: true)asserthtml_response(conn,200)=~"Welcome!"endend
  3. Typical workflow:

    # Run specific test + Pa11y in one command
    mix excessibility test/my_test.exs
    mix excessibility test/my_test.exs:42
    mix excessibility --only a11y
    # Or run tests separately, then check all snapshots
    mix test# Generates snapshots in test/excessibility/
    mix excessibility # Runs Pa11y against all snapshots# 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

useExcessibilityhtml_snapshot(conn,name: "homepage.html",screenshot?: true)

The html_snapshot/2 macro works with:

  • Plug.Conn
  • Wallaby.Session
  • Phoenix.LiveViewTest.View
  • Phoenix.LiveViewTest.Element

It returns the source unchanged, so you can use it in pipelines.

Options

OptionTypeDefaultDescription
:namestringauto-generatedCustom filename (e.g., "login_form.html"). Default is ModuleName_LineNumber.html
:screenshot?booleanfalseGenerate PNG screenshots (requires ChromicPDF)
:open_browser?booleanfalseOpen the snapshot in your browser after writing
:cleanup?booleanfalseDelete 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:

  1. Diff files are created.good.html (baseline) and .bad.html (new)
  2. Both open in your browser for visual comparison
  3. You choose which to keep — "good" to reject changes, "bad" to accept as new baseline
  4. 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 KeyRequiredDefaultDescription
:endpointYesYour Phoenix endpoint module (e.g., MyAppWeb.Endpoint)
:system_modNoExcessibility.SystemModule for system commands (mockable)
:browser_modNoWallaby.BrowserModule for browser interactions
:live_view_modNoExcessibility.LiveViewModule for LiveView rendering
:excessibility_output_pathNo"test/excessibility"Base directory for snapshots
:pa11y_pathNoauto-detectedPath to Pa11y executable
:pa11y_configNo"pa11y.json"Path to Pa11y config file
:head_render_pathNo"/"Route used for rendering <head> content
:custom_enrichersNo[]List of custom enricher modules (see Telemetry Analysis)
:custom_analyzersNo[]List of custom analyzer modules (see Telemetry Analysis)

Example:

# test/test_helper.exsApplication.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

TaskDescription
mix excessibility.installConfigure config/test.exs, create pa11y.json, install Pa11y via npm
mix excessibilityRun Pa11y against all existing snapshots
mix excessibility [test args]Run tests, then Pa11y on new snapshots (passthrough to mix test)
mix excessibility.baselineLock current snapshots as baseline
mix excessibility.compareCompare snapshots against baseline, resolve diffs interactively
mix excessibility.compare --keep goodKeep all baseline versions (reject changes)
mix excessibility.compare --keep badAccept all new versions as baseline
mix excessibility.debug [test args]Run tests with telemetry, generate debug report (passthrough to mix test)
mix excessibility.debug [test args] --format=jsonOutput debug report as JSON
mix excessibility.debug [test args] --format=packageCreate debug package directory
mix excessibility.latestDisplay most recent debug report
mix excessibility.package [test]Create debug package (alias for --format=package)
mix excessibility.setup_claude_docsCreate/update .claude_docs/excessibility.md

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.exsMox.defmock(Excessibility.SystemMock,for: Excessibility.SystemBehaviour)Application.put_env(:excessibility,:system_mod,Excessibility.SystemMock)

Then stub in your tests:

importMoxsetup:verify_on_exit!test"snapshot without browser open",%{conn: conn}doExcessibility.SystemMock|>stub(:open_with_system_cmd,fn_path->:okend)conn=get(conn,"/")html_snapshot(conn,open_browser?: true)# Won't actually openend

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

About

Accessibility snapshot testing for Phoenix LiveView - capture HTML during tests, run Pa11y for WCAG compliance, debug with AI-friendly timeline analysis

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages