Skip to content

feat: Scanner API (#107) + LiveView rules framework (#101)#108

Merged
lessthanseventy merged 1 commit into
mainfrom
feat/scanner-and-lv-rules
Apr 11, 2026
Merged

feat: Scanner API (#107) + LiveView rules framework (#101)#108
lessthanseventy merged 1 commit into
mainfrom
feat/scanner-and-lv-rules

Conversation

@lessthanseventy
Copy link
Copy Markdown
Owner

Summary

Promotes the axe-core scanning logic into a stable public API so it
can be called from runtime application code (LiveViews, Oban jobs,
CLI wrappers), and introduces a complementary LiveView-aware rule
subsystem that flags Phoenix-specific a11y anti-patterns axe-core
can't see. First rule: phx-click on non-interactive elements.

Bumps the library to 0.13.0.

Excessibility.Scanner — runtime scanning API (closes #107)

  • Excessibility.Scanner.scan/2 returns {:ok, report} with atom-keyed
    violations and normalized :impact atoms, plus metadata: :final_url
    (after redirects), :duration_ms, :engine.axe_version,
    :engine.chromium_version, :passes_count, :inapplicable_count,
    :timestamp, :fallback.
  • Typed error tuples — :timeout, {:http_error, status},
    {:navigation_failed, msg}, {:playwright_error, msg},
    {:invalid_url, reason} — so LiveView handlers can pattern-match
    cleanly.
  • Options: :timeout, :wait_for, :wait_until, :viewport, :tags,
    :user_agent, :screenshot, :disable_rules, :fallback.
  • assets/axe-runner.js rewritten to emit the richer JSON and accept
    --timeout, --wait-until, --tags, --viewport, --user-agent.
  • Excessibility.AxeRunner removed; curl-fallback logic folded into
    Scanner as a private helper.
  • All internal callers migrated: snapshot.ex (screenshot capture),
    mix excessibility, mix excessibility.check (now a thin wrapper),
    MCP a11y_check tool.

Unblocks: the lessthanseventy.com/check public URL scanner —
just pin {:excessibility, "~> 0.13"} and call
Excessibility.Scanner.scan/2 from a LiveView handler.

Excessibility.LiveViewRules — LiveView-aware rules (closes #101)

  • New Excessibility.LiveViewRules.Rule behaviour with compile-time
    auto-discovery from lib/excessibility/live_view_rules/rules/.
    Follows the same pattern as the existing TelemetryCapture.Registry.
  • Custom rules register via :custom_live_view_rules in app config.
  • First rule :phx_click_on_non_interactive flags phx-click
    and phx-click-away on anything that isn't natively keyboard-
    accessible: not <a> / <button> / <input> / <select> /
    <textarea> / <summary> / <details>, no tabindex, no
    interactive role.
  • mix excessibility now runs both axe-core and the rules on each
    snapshot and fails if either finds issues.
  • Non-Phoenix safe: if the HTML has no phx-* attributes, rules
    are no-ops and produce zero findings.
  • Config knobs: :lv_rules_enabled? (default true),
    :lv_rules_disabled (list of rule ids to skip).

Flake fix (drive-by)

While hardening the test suite for this PR I hit a pre-existing
intermittent failure: concurrent on_exit callbacks in
ecto_queries_test.exs and push_events_test.exs racing on the
shared Agent-backed store. The existing Process.whereis/1 guard
in stop_store/0 has a TOCTOU hole. Fixed by making stop_store/0
idempotent via try/catch :exit, _. Tests are now stable across
varied seeds (ran 5 seeds green).

Version bump

  • mix.exs: 0.12.1 → 0.13.0
  • lib/excessibility/mcp/server.ex (MCP server_info): 0.12.0 → 0.13.0
  • README.md dep pin: ~> 0.12~> 0.13
  • CHANGELOG.md: new 0.13.0 section
  • README.md: new "LiveView-Aware Rules" and "Runtime Usage (Scanner API)" sections

Still open (not in this PR)

The sibling issues #102, #103, #104, #105, #106 are not
implemented here. The framework landed in this PR makes each a
single-module addition — the next rule just goes in
lib/excessibility/live_view_rules/rules/ and is picked up
automatically at compile time.

Test plan

  • mix test — 606 tests passing
  • Stability: mix test --seed N across 5 varied seeds — all green
  • mix format --check-formatted
  • MIX_ENV=test mix credo --strict — zero issues
  • mix compile --warnings-as-errors
  • Manually verify mix excessibility.check https://example.com still works against a real remote URL
  • Dog-food: pin {:excessibility, "~> 0.13"} in lessthanseventy.com and wire /check LiveView to Excessibility.Scanner.scan/2

🤖 Generated with Claude Code

Promote the axe-core scanning logic into a stable public module and
add a new LiveView-aware rule subsystem that catches Phoenix-specific
accessibility anti-patterns axe-core can't see.

Scanner (#107):
- Excessibility.Scanner.scan/2 is now the public runtime API — callable
  from LiveViews, Oban jobs, or any application code (not just Mix).
- Returns a typed report: atom-keyed violations, :impact atoms, and
  metadata (final_url, duration_ms, engine.axe/chromium versions,
  passes_count, inapplicable_count).
- Returns typed error tuples: :timeout, {:http_error, status},
  {:navigation_failed, msg}, {:playwright_error, msg}, {:invalid_url, r}.
- assets/axe-runner.js rewritten to emit the richer JSON and new error
  shape, and to accept --timeout, --wait-until, --tags, --viewport,
  --user-agent flags.
- Excessibility.AxeRunner deleted; its curl-fallback logic folded into
  Scanner. All callers migrated (snapshot.ex, mix excessibility,
  mix excessibility.check, MCP a11y_check tool).
- mix excessibility.check is now a thin wrapper over Scanner.

LiveView rules (#101):
- New Excessibility.LiveViewRules subsystem with Rule behaviour and
  compile-time auto-discovery from lib/excessibility/live_view_rules/
  rules/. Custom rules register via :custom_live_view_rules config.
- First rule :phx_click_on_non_interactive flags phx-click /
  phx-click-away on elements that aren't natively keyboard-accessible
  (not <a>/<button>/<input>/<select>/<textarea>/<summary>/<details>,
  no tabindex, no interactive role).
- mix excessibility runs both axe-core and the rules on each snapshot.
- Rules are no-ops on non-Phoenix HTML, so this is safe for projects
  that don't use LiveView.
- Config: :lv_rules_enabled? (default true), :lv_rules_disabled.

Flake fix:
- Guard stop_store/0 in ecto_queries.ex and push_events.ex against
  races between concurrent on_exit callbacks on the shared Agent.
  Tests are now stable across varied seeds.

Version bump: 0.12.1 -> 0.13.0 (mix.exs, MCP server info, README dep
pin, CHANGELOG entry).

Closes #107
Closes #101

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lessthanseventy lessthanseventy merged commit f6545f3 into main Apr 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extract scanning logic into public Excessibility.Scanner module LiveView-aware rule: phx-click on non-interactive elements

1 participant