Excessibility.Scanner (Excessibility v0.14.0)
View SourceRuntime scanner for arbitrary URLs.
Unlike the ExUnit integration in Excessibility, this module is intended
to be called from application code — LiveViews, background jobs, CLI
wrappers, external HTTP APIs, etc. It launches Playwright (via
assets/axe-runner.js), navigates to the given URL, and runs axe-core
analysis, returning a structured report.
Usage
{:ok, report} = Excessibility.Scanner.scan("https://example.com")
for v <- report.violations do
IO.puts("[#{v.impact}] #{v.id}: #{v.description}")
endOn failure, returns {:error, reason} where reason is a typed tuple
(see scan_error/0). Pattern-match cleanly from LiveView handlers:
case Excessibility.Scanner.scan(url, timeout: 20_000) do
{:ok, report} -> send(self(), {:scan_complete, report})
{:error, :timeout} -> send(self(), {:scan_failed, :timeout})
{:error, {:http_error, status}} -> ...
{:error, {:navigation_failed, msg}} -> ...
{:error, {:invalid_url, _}} -> ...
{:error, {:playwright_error, msg}} -> ...
endFallback behavior
If Playwright fails to reach a remote URL (timeout, WAF block, or
navigation error), the scanner automatically retries by fetching the
HTML via curl and scanning it as a local file. This won't execute
JavaScript, so SPA content may be missing, but server-rendered pages
still get full results. When the fallback path is used, the returned
report has a non-nil :fallback field. Disable with fallback: false.
file:// URLs never fall back (curl can't fetch them).
Summary
Types
Engine metadata for a scan.
Metadata describing a curl fallback, when one was used.
axe-core impact level, normalized to an atom.
A single offending element within a violation.
A complete scan report.
Structured scan failure.
Options accepted by scan/2.
A single axe-core violation.
Functions
Scan a URL and return a structured accessibility report.
Types
Engine metadata for a scan.
Metadata describing a curl fallback, when one was used.
@type impact() :: :critical | :serious | :moderate | :minor | nil
axe-core impact level, normalized to an atom.
A single offending element within a violation.
@type report() :: %{ url: String.t(), final_url: String.t(), violations: [violation()], incomplete: [violation()], passes_count: non_neg_integer(), inapplicable_count: non_neg_integer(), timestamp: DateTime.t(), duration_ms: non_neg_integer(), engine: engine_info(), fallback: fallback_info() }
A complete scan report.
@type scan_error() :: :timeout | {:http_error, non_neg_integer()} | {:navigation_failed, String.t()} | {:playwright_error, String.t()} | {:invalid_url, atom()}
Structured scan failure.
@type scan_opts() :: [ timeout: pos_integer(), wait_for: String.t(), wait_until: :load | :domcontentloaded | :networkidle, viewport: {pos_integer(), pos_integer()}, tags: [String.t()], user_agent: String.t() | nil, screenshot: Path.t() | nil, disable_rules: [String.t()], fallback: boolean() ]
Options accepted by scan/2.
@type violation() :: %{ id: String.t(), impact: impact(), description: String.t(), help: String.t(), help_url: String.t(), tags: [String.t()], nodes: [node_info()] }
A single axe-core violation.
Functions
@spec scan(String.t(), scan_opts()) :: {:ok, report()} | {:error, scan_error()}
Scan a URL and return a structured accessibility report.
Options
:timeout— Navigation/analysis timeout in ms (default:30_000):wait_for— CSS selector to wait for before running axe:wait_until— Playwright wait state::load|:domcontentloaded|:networkidle(default::loadfor remote,:domcontentloadedfor file):viewport—{width, height}tuple (default:{1280, 720}):tags— axe-core tag filter (default:["wcag2a", "wcag2aa"]):user_agent— Override the default Chrome UA string:screenshot— Path to save a full-page PNG:disable_rules— List of axe rule IDs to skip:fallback— Fall back to curl + file:// on Playwright failure (default:true, remote URLs only)
Returns
{:ok, report} on success or {:error, reason} where reason is one of
the scan_error/0 tuples.