feat: Scanner API (#107) + LiveView rules framework (#101)#108
Merged
Conversation
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>
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-clickon non-interactive elements.Bumps the library to 0.13.0.
Excessibility.Scanner— runtime scanning API (closes #107)Excessibility.Scanner.scan/2returns{:ok, report}with atom-keyedviolations and normalized
:impactatoms, plus metadata::final_url(after redirects),
:duration_ms,:engine.axe_version,:engine.chromium_version,:passes_count,:inapplicable_count,:timestamp,:fallback.:timeout,{:http_error, status},{:navigation_failed, msg},{:playwright_error, msg},{:invalid_url, reason}— so LiveView handlers can pattern-matchcleanly.
:timeout,:wait_for,:wait_until,:viewport,:tags,:user_agent,:screenshot,:disable_rules,:fallback.assets/axe-runner.jsrewritten to emit the richer JSON and accept--timeout,--wait-until,--tags,--viewport,--user-agent.Excessibility.AxeRunnerremoved; curl-fallback logic folded intoScanneras a private helper.snapshot.ex(screenshot capture),mix excessibility,mix excessibility.check(now a thin wrapper),MCP
a11y_checktool.Unblocks: the
lessthanseventy.com/checkpublic URL scanner —just pin
{:excessibility, "~> 0.13"}and callExcessibility.Scanner.scan/2from a LiveView handler.Excessibility.LiveViewRules— LiveView-aware rules (closes #101)Excessibility.LiveViewRules.Rulebehaviour with compile-timeauto-discovery from
lib/excessibility/live_view_rules/rules/.Follows the same pattern as the existing
TelemetryCapture.Registry.:custom_live_view_rulesin app config.:phx_click_on_non_interactiveflagsphx-clickand
phx-click-awayon anything that isn't natively keyboard-accessible: not
<a>/<button>/<input>/<select>/<textarea>/<summary>/<details>, notabindex, nointeractive
role.mix excessibilitynow runs both axe-core and the rules on eachsnapshot and fails if either finds issues.
phx-*attributes, rulesare no-ops and produce zero findings.
:lv_rules_enabled?(defaulttrue),: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_exitcallbacks inecto_queries_test.exsandpush_events_test.exsracing on theshared
Agent-backed store. The existingProcess.whereis/1guardin
stop_store/0has a TOCTOU hole. Fixed by makingstop_store/0idempotent via
try/catch :exit, _. Tests are now stable acrossvaried seeds (ran 5 seeds green).
Version bump
mix.exs: 0.12.1 → 0.13.0lib/excessibility/mcp/server.ex(MCPserver_info): 0.12.0 → 0.13.0README.mddep pin:~> 0.12→~> 0.13CHANGELOG.md: new 0.13.0 sectionREADME.md: new "LiveView-Aware Rules" and "Runtime Usage (Scanner API)" sectionsStill 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 upautomatically at compile time.
Test plan
mix test— 606 tests passingmix test --seed Nacross 5 varied seeds — all greenmix format --check-formattedMIX_ENV=test mix credo --strict— zero issuesmix compile --warnings-as-errorsmix excessibility.check https://example.comstill works against a real remote URL{:excessibility, "~> 0.13"}inlessthanseventy.comand wire/checkLiveView toExcessibility.Scanner.scan/2🤖 Generated with Claude Code