Excessibility.Scanner (Excessibility v0.14.0)

View Source

Runtime 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}")
end

On 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}} -> ...
end

Fallback 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_info()

@type engine_info() :: %{
  axe_version: String.t() | nil,
  chromium_version: String.t() | nil
}

Engine metadata for a scan.

fallback_info()

@type fallback_info() :: %{method: atom(), original_error: term()} | nil

Metadata describing a curl fallback, when one was used.

impact()

@type impact() :: :critical | :serious | :moderate | :minor | nil

axe-core impact level, normalized to an atom.

node_info()

@type node_info() :: %{
  target: [String.t()],
  html: String.t(),
  failure_summary: String.t()
}

A single offending element within a violation.

report()

@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.

scan_error()

@type scan_error() ::
  :timeout
  | {:http_error, non_neg_integer()}
  | {:navigation_failed, String.t()}
  | {:playwright_error, String.t()}
  | {:invalid_url, atom()}

Structured scan failure.

scan_opts()

@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.

violation()

@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

scan(url, opts \\ [])

@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: :load for remote, :domcontentloaded for 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.