@diegovelasquezweb/a11y-engine 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +149 -7
- package/docs/architecture.md +139 -0
- package/docs/cli-handbook.md +209 -0
- package/docs/outputs.md +245 -0
- package/package.json +7 -4
- package/scripts/engine/dom-scanner.mjs +310 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [0.1.2] — 2026-03-13
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `bin` field in `package.json` — removed leading `./` from the entry path (`scripts/audit.mjs`) to satisfy npm bin resolution
|
|
17
|
+
- `repository.url` normalized to `git+https://` prefix as required by npm registry validation
|
|
18
|
+
- Missing shebang (`#!/usr/bin/env node`) added to `scripts/audit.mjs` so the `a11y-audit` binary executes correctly when installed globally or via `npx`
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [0.1.1] — 2026-03-13
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Engine scripts published as a standalone npm package:
|
|
27
|
+
- `scripts/audit.mjs` — orchestrator for the full audit pipeline
|
|
28
|
+
- `scripts/core/utils.mjs` — shared logging, path utilities, and defaults
|
|
29
|
+
- `scripts/core/toolchain.mjs` — dependency and Playwright browser verification
|
|
30
|
+
- `scripts/core/asset-loader.mjs` — JSON asset loading with error boundaries
|
|
31
|
+
- `scripts/engine/dom-scanner.mjs` — Playwright + axe-core WCAG 2.2 AA scanner
|
|
32
|
+
- `scripts/engine/analyzer.mjs` — finding enrichment with fix intelligence
|
|
33
|
+
- `scripts/engine/source-scanner.mjs` — static source code pattern scanner
|
|
34
|
+
- `scripts/reports/builders/` — orchestrators for each report format
|
|
35
|
+
- `scripts/reports/renderers/` — rendering logic for HTML, PDF, Markdown, and checklist
|
|
36
|
+
- Asset files bundled under `assets/`:
|
|
37
|
+
- `assets/reporting/compliance-config.json` — scoring weights, grade thresholds, and legal regulation mapping
|
|
38
|
+
- `assets/reporting/wcag-reference.json` — WCAG criterion map, persona config, and persona–rule mapping
|
|
39
|
+
- `assets/reporting/manual-checks.json` — 41 manual WCAG checks for the interactive checklist
|
|
40
|
+
- `assets/discovery/crawler-config.json` — BFS crawl configuration defaults
|
|
41
|
+
- `assets/discovery/stack-detection.json` — framework and CMS fingerprint signatures
|
|
42
|
+
- `assets/remediation/intelligence.json` — per-rule fix intelligence (106 axe-core rules)
|
|
43
|
+
- `assets/remediation/code-patterns.json` — source code pattern definitions
|
|
44
|
+
- `assets/remediation/guardrails.json` — agent fix guardrails and scope rules
|
|
45
|
+
- `assets/remediation/axe-check-maps.json` — axe check-to-rule mapping
|
|
46
|
+
- `assets/remediation/source-boundaries.json` — framework-specific file location patterns
|
|
47
|
+
- `a11y-audit` binary registered in `bin` field — invocable via `npx a11y-audit` after install
|
|
48
|
+
- `LICENSE` (MIT)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [0.1.0] — 2026-03-13
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- Initial package scaffold: `package.json` for `@diegovelasquezweb/a11y-engine` with correct `name`, `version`, `type: module`, `engines`, `files`, and `scripts` fields
|
|
57
|
+
- `devDependencies`: `vitest` for test runner
|
|
58
|
+
- `dependencies`: `playwright`, `@axe-core/playwright`, `axe-core`, `pa11y`
|
package/README.md
CHANGED
|
@@ -1,20 +1,162 @@
|
|
|
1
1
|
# @diegovelasquezweb/a11y-engine
|
|
2
2
|
|
|
3
|
-
WCAG 2.2 AA accessibility audit engine.
|
|
3
|
+
WCAG 2.2 AA accessibility audit engine. Runs Playwright + axe-core scans, enriches findings with fix intelligence, and produces structured artifacts for developers, agents, and stakeholders.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
A Node.js CLI and programmatic engine that:
|
|
8
|
+
|
|
9
|
+
1. Crawls a target URL and discovers routes automatically
|
|
10
|
+
2. Runs axe-core WCAG 2.2 AA checks across all discovered pages
|
|
11
|
+
3. Optionally scans project source code for patterns axe cannot detect at runtime
|
|
12
|
+
4. Enriches each finding with stack-aware fix guidance, selectors, and verification commands
|
|
13
|
+
5. Produces a full artifact set: JSON data, Markdown remediation guide, HTML dashboard, PDF compliance report, and manual testing checklist
|
|
14
|
+
|
|
15
|
+
## Why use this engine
|
|
16
|
+
|
|
17
|
+
| Capability | With this engine | Without |
|
|
18
|
+
| :--- | :--- | :--- |
|
|
19
|
+
| **Full WCAG 2.2 Coverage** | axe-core runtime scan + source code pattern scanner | Runtime scan only — misses CSS/source-level issues |
|
|
20
|
+
| **Fix Intelligence** | Stack-aware patches with code snippets tailored to detected framework | Raw rule violations with no remediation context |
|
|
21
|
+
| **Structured Artifacts** | JSON + Markdown + HTML + PDF + Checklist — ready to consume or forward | Findings exist only in the terminal session |
|
|
22
|
+
| **CI/Agent Integration** | Deterministic exit codes, stdout-parseable output paths, JSON schema | Requires wrapper scripting |
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
6
25
|
|
|
7
26
|
```bash
|
|
8
|
-
npm
|
|
27
|
+
npm install @diegovelasquezweb/a11y-engine
|
|
9
28
|
npx playwright install chromium
|
|
10
29
|
```
|
|
11
30
|
|
|
12
|
-
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add @diegovelasquezweb/a11y-engine
|
|
33
|
+
pnpm exec playwright install chromium
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> Chromium must be installed separately. The engine uses Playwright's bundled browser — not a system Chrome.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Minimal scan — produces remediation.md in .audit/
|
|
42
|
+
npx a11y-audit --base-url https://example.com
|
|
43
|
+
|
|
44
|
+
# Full audit with all reports
|
|
45
|
+
npx a11y-audit --base-url https://example.com --with-reports --output ./audit/report.html
|
|
46
|
+
|
|
47
|
+
# Scan with source code intelligence (for stack-aware fix guidance)
|
|
48
|
+
npx a11y-audit --base-url http://localhost:3000 --project-dir . --with-reports --output ./audit/report.html
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI usage
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
a11y-audit --base-url <url> [options]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Targeting & scope
|
|
58
|
+
|
|
59
|
+
| Flag | Argument | Default | Description |
|
|
60
|
+
| :--- | :--- | :--- | :--- |
|
|
61
|
+
| `--base-url` | `<url>` | (Required) | Starting URL for the audit. |
|
|
62
|
+
| `--max-routes` | `<num>` | `10` | Max routes to discover and scan. |
|
|
63
|
+
| `--crawl-depth` | `<num>` | `2` | BFS link-follow depth during discovery (1–3). |
|
|
64
|
+
| `--routes` | `<csv>` | — | Explicit path list, bypasses auto-discovery. |
|
|
65
|
+
| `--project-dir` | `<path>` | — | Path to project source. Enables source pattern scanner and framework auto-detection. |
|
|
66
|
+
|
|
67
|
+
### Audit intelligence
|
|
68
|
+
|
|
69
|
+
| Flag | Argument | Default | Description |
|
|
70
|
+
| :--- | :--- | :--- | :--- |
|
|
71
|
+
| `--target` | `<text>` | `WCAG 2.2 AA` | Compliance target label in reports. |
|
|
72
|
+
| `--only-rule` | `<id>` | — | Run a single axe rule (e.g. `color-contrast`). |
|
|
73
|
+
| `--ignore-findings` | `<csv>` | — | Rule IDs to exclude from output. |
|
|
74
|
+
| `--exclude-selectors` | `<csv>` | — | CSS selectors to skip during DOM scan. |
|
|
75
|
+
| `--framework` | `<name>` | — | Override auto-detected stack. Supported: `nextjs`, `gatsby`, `react`, `nuxt`, `vue`, `angular`, `astro`, `svelte`, `shopify`, `wordpress`, `drupal`. |
|
|
76
|
+
|
|
77
|
+
### Execution & emulation
|
|
78
|
+
|
|
79
|
+
| Flag | Argument | Default | Description |
|
|
80
|
+
| :--- | :--- | :--- | :--- |
|
|
81
|
+
| `--color-scheme` | `light\|dark` | `light` | Emulate `prefers-color-scheme`. |
|
|
82
|
+
| `--wait-until` | `domcontentloaded\|load\|networkidle` | `domcontentloaded` | Playwright page load strategy. Use `networkidle` for SPAs. |
|
|
83
|
+
| `--viewport` | `<WxH>` | — | Viewport size (e.g. `375x812`, `1440x900`). |
|
|
84
|
+
| `--wait-ms` | `<num>` | `2000` | Delay after page load before running axe (ms). |
|
|
85
|
+
| `--timeout-ms` | `<num>` | `30000` | Network timeout per page (ms). |
|
|
86
|
+
| `--headed` | — | `false` | Run browser in visible mode. |
|
|
87
|
+
| `--affected-only` | — | `false` | Re-scan only routes with previous violations. Requires a prior scan in `.audit/`. |
|
|
88
|
+
|
|
89
|
+
### Output generation
|
|
90
|
+
|
|
91
|
+
| Flag | Argument | Default | Description |
|
|
92
|
+
| :--- | :--- | :--- | :--- |
|
|
93
|
+
| `--with-reports` | — | `false` | Generate HTML + PDF + Checklist reports. Requires `--output`. |
|
|
94
|
+
| `--skip-reports` | — | `true` | Skip visual report generation (default). |
|
|
95
|
+
| `--output` | `<path>` | — | Output path for `report.html` (PDF and checklist derive from it). |
|
|
96
|
+
| `--skip-patterns` | — | `false` | Disable source code pattern scanner even when `--project-dir` is set. |
|
|
97
|
+
|
|
98
|
+
## Common command patterns
|
|
13
99
|
|
|
14
100
|
```bash
|
|
15
|
-
|
|
101
|
+
# Focused audit — one rule, one route
|
|
102
|
+
a11y-audit --base-url https://example.com --only-rule color-contrast --routes /checkout --max-routes 1
|
|
103
|
+
|
|
104
|
+
# Dark mode audit
|
|
105
|
+
a11y-audit --base-url https://example.com --color-scheme dark
|
|
106
|
+
|
|
107
|
+
# SPA with deferred rendering
|
|
108
|
+
a11y-audit --base-url https://example.com --wait-until networkidle --wait-ms 3000
|
|
109
|
+
|
|
110
|
+
# Mobile viewport
|
|
111
|
+
a11y-audit --base-url https://example.com --viewport 375x812
|
|
112
|
+
|
|
113
|
+
# Fast re-audit after fixes (skips clean pages)
|
|
114
|
+
a11y-audit --base-url https://example.com --affected-only
|
|
115
|
+
|
|
116
|
+
# Ignore known false positives
|
|
117
|
+
a11y-audit --base-url https://example.com --ignore-findings color-contrast,frame-title
|
|
16
118
|
```
|
|
17
119
|
|
|
18
|
-
##
|
|
120
|
+
## Output artifacts
|
|
121
|
+
|
|
122
|
+
All artifacts are written to `.audit/` relative to the package root.
|
|
123
|
+
|
|
124
|
+
| File | Always generated | Description |
|
|
125
|
+
| :--- | :--- | :--- |
|
|
126
|
+
| `a11y-scan-results.json` | Yes | Raw axe-core results per route |
|
|
127
|
+
| `a11y-findings.json` | Yes | Enriched findings with fix intelligence |
|
|
128
|
+
| `remediation.md` | Yes | AI-agent-optimized remediation roadmap |
|
|
129
|
+
| `report.html` | With `--with-reports` | Interactive HTML dashboard |
|
|
130
|
+
| `report.pdf` | With `--with-reports` | Formal compliance PDF |
|
|
131
|
+
| `checklist.html` | With `--with-reports` | Manual WCAG testing checklist |
|
|
132
|
+
|
|
133
|
+
See [Output Artifacts](docs/outputs.md) for full schema reference.
|
|
134
|
+
|
|
135
|
+
## Troubleshooting
|
|
136
|
+
|
|
137
|
+
**`Error: browserType.launch: Executable doesn't exist`**
|
|
138
|
+
Run `npx playwright install chromium` (or `pnpm exec playwright install chromium`).
|
|
139
|
+
|
|
140
|
+
**`Missing required argument: --base-url`**
|
|
141
|
+
The flag is required. Provide a full URL including protocol: `--base-url https://example.com`.
|
|
142
|
+
|
|
143
|
+
**Scan returns 0 findings on an SPA**
|
|
144
|
+
Use `--wait-until networkidle --wait-ms 3000` to let async content render before axe runs.
|
|
145
|
+
|
|
146
|
+
**`--with-reports` exits without generating PDF**
|
|
147
|
+
Ensure `--output` is also set and points to an `.html` file path: `--output ./audit/report.html`.
|
|
148
|
+
|
|
149
|
+
**Chromium crashes in CI**
|
|
150
|
+
Add `--no-sandbox` via the `PLAYWRIGHT_CHROMIUM_LAUNCH_OPTIONS` env var, or run Playwright with the `--with-deps` flag during browser installation.
|
|
151
|
+
|
|
152
|
+
## Documentation
|
|
153
|
+
|
|
154
|
+
| Resource | Description |
|
|
155
|
+
| :--- | :--- |
|
|
156
|
+
| [Architecture](https://github.com/diegovelasquezweb/a11y-engine/blob/main/docs/architecture.md) | How the scanner → analyzer → report pipeline works |
|
|
157
|
+
| [CLI Handbook](https://github.com/diegovelasquezweb/a11y-engine/blob/main/docs/cli-handbook.md) | Full flag reference and usage patterns |
|
|
158
|
+
| [Output Artifacts](https://github.com/diegovelasquezweb/a11y-engine/blob/main/docs/outputs.md) | Schema and structure of every generated file |
|
|
159
|
+
|
|
160
|
+
## License
|
|
19
161
|
|
|
20
|
-
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Engine Architecture
|
|
2
|
+
|
|
3
|
+
**Navigation**: [Home](../README.md) • [Architecture](architecture.md) • [CLI Handbook](cli-handbook.md) • [Output Artifacts](outputs.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Pipeline overview](#pipeline-overview)
|
|
10
|
+
- [Stage 1: DOM scanner](#stage-1-dom-scanner)
|
|
11
|
+
- [Stage 2: Analyzer](#stage-2-analyzer)
|
|
12
|
+
- [Stage 3: Report builders](#stage-3-report-builders)
|
|
13
|
+
- [Assets and rule intelligence](#assets-and-rule-intelligence)
|
|
14
|
+
- [Execution model and timeouts](#execution-model-and-timeouts)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
The engine operates as a three-stage pipeline. Each stage is an independent Node.js process spawned by `audit.mjs`. Stages communicate through JSON files written to `.audit/`.
|
|
19
|
+
|
|
20
|
+
## Pipeline overview
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Target URL
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
┌─────────────────────────────┐
|
|
27
|
+
│ Stage 1: DOM Scanner │ Playwright + axe-core
|
|
28
|
+
│ dom-scanner.mjs │ Route discovery + WCAG scan
|
|
29
|
+
└──────────────┬──────────────┘
|
|
30
|
+
│ a11y-scan-results.json
|
|
31
|
+
▼
|
|
32
|
+
┌─────────────────────────────┐
|
|
33
|
+
│ Stage 1b: Source Scanner │ Static regex analysis
|
|
34
|
+
│ source-scanner.mjs │ (optional — requires --project-dir)
|
|
35
|
+
└──────────────┬──────────────┘
|
|
36
|
+
│ merges into a11y-findings.json
|
|
37
|
+
▼
|
|
38
|
+
┌─────────────────────────────┐
|
|
39
|
+
│ Stage 2: Analyzer │ Fix intelligence enrichment
|
|
40
|
+
│ analyzer.mjs │ intelligence.json + guardrails
|
|
41
|
+
└──────────────┬──────────────┘
|
|
42
|
+
│ a11y-findings.json
|
|
43
|
+
▼
|
|
44
|
+
┌─────────────────────────────┐
|
|
45
|
+
│ Stage 3: Report Builders │ Parallel rendering
|
|
46
|
+
│ md / html / pdf / checklist│
|
|
47
|
+
└──────────────┬──────────────┘
|
|
48
|
+
│
|
|
49
|
+
┌──────────┼──────────┬──────────────┐
|
|
50
|
+
▼ ▼ ▼ ▼
|
|
51
|
+
remediation report report checklist
|
|
52
|
+
.md .html .pdf .html
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Stage 1: DOM scanner
|
|
56
|
+
|
|
57
|
+
**Script**: `scripts/engine/dom-scanner.mjs`
|
|
58
|
+
|
|
59
|
+
Launches a Playwright-controlled Chromium browser and runs axe-core against each discovered route.
|
|
60
|
+
|
|
61
|
+
**Route discovery**:
|
|
62
|
+
- If the site exposes a `sitemap.xml`, all listed URLs are scanned (up to `--max-routes`).
|
|
63
|
+
- Otherwise, BFS crawl starting from `--base-url`, following same-origin `<a href>` links up to `--crawl-depth` levels deep.
|
|
64
|
+
- Routes are deduplicated and normalized before scanning.
|
|
65
|
+
|
|
66
|
+
**Scanning**:
|
|
67
|
+
- 3 parallel browser tabs scan routes concurrently (~2–3× faster than sequential).
|
|
68
|
+
- axe-core 4.11+ runs WCAG 2.2 A, AA, and best-practice tag sets.
|
|
69
|
+
- Screenshots of affected elements are captured for each violation.
|
|
70
|
+
- `--color-scheme`, `--viewport`, `--wait-until`, and `--wait-ms` control the browser environment.
|
|
71
|
+
|
|
72
|
+
**Output**: `a11y-scan-results.json` — raw axe results per route with DOM snapshots.
|
|
73
|
+
|
|
74
|
+
### Optional: Source scanner
|
|
75
|
+
|
|
76
|
+
**Script**: `scripts/engine/source-scanner.mjs` — runs when `--project-dir` is set and `--skip-patterns` is not.
|
|
77
|
+
|
|
78
|
+
Performs static analysis of source files for accessibility issues axe cannot detect at runtime (e.g. focus outline suppression, missing alt text in templates). Uses regex patterns from `assets/remediation/code-patterns.json` scoped to framework-specific file boundaries from `assets/remediation/source-boundaries.json`.
|
|
79
|
+
|
|
80
|
+
Findings are classified as `confirmed` (pattern unambiguously matches) or `potential` (requires human verification).
|
|
81
|
+
|
|
82
|
+
## Stage 2: Analyzer
|
|
83
|
+
|
|
84
|
+
**Script**: `scripts/engine/analyzer.mjs`
|
|
85
|
+
|
|
86
|
+
Reads `a11y-scan-results.json` and enriches each violation with:
|
|
87
|
+
|
|
88
|
+
- **Fix intelligence** from `assets/remediation/intelligence.json` — 106 axe-core rules with code snippets, MDN links, framework-specific notes, and WCAG criterion mapping.
|
|
89
|
+
- **Selector scoring** — picks the most stable selector from axe's `nodes` list. Priority: `#id` > `[data-*]` > `[aria-*]` > `[type=]`, with penalty for Tailwind utility classes.
|
|
90
|
+
- **Framework context** — `assets/discovery/stack-detection.json` fingerprints the DOM to detect framework and CMS. Per-finding `framework_notes` and `cms_notes` are filtered to the detected stack.
|
|
91
|
+
- **Guardrails** — `assets/remediation/guardrails.json` defines scope rules that prevent agents from touching backend code, third-party scripts, or minified files.
|
|
92
|
+
- **Compliance scoring** — `assets/reporting/compliance-config.json` weights findings by severity to produce a 0–100 score with grade thresholds.
|
|
93
|
+
- **Persona impact groups** — `assets/reporting/wcag-reference.json` maps findings to disability personas (visual, motor, cognitive, etc.).
|
|
94
|
+
|
|
95
|
+
**Output**: `a11y-findings.json` — enriched findings array with all intelligence fields.
|
|
96
|
+
|
|
97
|
+
## Stage 3: Report builders
|
|
98
|
+
|
|
99
|
+
All builders run in parallel when `--with-reports` is set. Each reads `a11y-findings.json` independently.
|
|
100
|
+
|
|
101
|
+
| Builder | Script | Output | Audience |
|
|
102
|
+
| :--- | :--- | :--- | :--- |
|
|
103
|
+
| Markdown | `reports/builders/md.mjs` | `remediation.md` | AI agents |
|
|
104
|
+
| HTML | `reports/builders/html.mjs` | `report.html` | Developers |
|
|
105
|
+
| PDF | `reports/builders/pdf.mjs` | `report.pdf` | Stakeholders |
|
|
106
|
+
| Checklist | `reports/builders/checklist.mjs` | `checklist.html` | QA / Developers |
|
|
107
|
+
|
|
108
|
+
The `remediation.md` builder always runs (even without `--with-reports`) since it is the primary output for AI agent consumption.
|
|
109
|
+
|
|
110
|
+
Renderers in `scripts/reports/renderers/` contain the actual rendering logic — builders are thin orchestrators that call renderers and write output files.
|
|
111
|
+
|
|
112
|
+
## Assets and rule intelligence
|
|
113
|
+
|
|
114
|
+
Assets are static JSON files bundled with the package under `assets/`. They are read at runtime by the analyzer and report builders.
|
|
115
|
+
|
|
116
|
+
| Asset | Purpose |
|
|
117
|
+
| :--- | :--- |
|
|
118
|
+
| `reporting/compliance-config.json` | Score weights, grade thresholds, legal regulation list |
|
|
119
|
+
| `reporting/wcag-reference.json` | WCAG criterion map, persona config, persona–rule mapping |
|
|
120
|
+
| `reporting/manual-checks.json` | 41 manual checks for the WCAG checklist |
|
|
121
|
+
| `discovery/crawler-config.json` | BFS crawl defaults (timeouts, concurrency) |
|
|
122
|
+
| `discovery/stack-detection.json` | Framework/CMS DOM fingerprints |
|
|
123
|
+
| `remediation/intelligence.json` | Per-rule fix intelligence for 106 axe-core rules |
|
|
124
|
+
| `remediation/code-patterns.json` | Source code pattern definitions |
|
|
125
|
+
| `remediation/guardrails.json` | Agent fix scope guardrails |
|
|
126
|
+
| `remediation/axe-check-maps.json` | axe check-to-rule mapping |
|
|
127
|
+
| `remediation/source-boundaries.json` | Framework-specific source file locations |
|
|
128
|
+
|
|
129
|
+
## Execution model and timeouts
|
|
130
|
+
|
|
131
|
+
`audit.mjs` spawns each stage as a child process via `node:child_process`. All child processes:
|
|
132
|
+
|
|
133
|
+
- Inherit the parent's environment
|
|
134
|
+
- Run with `cwd` set to the package root (`SKILL_ROOT`)
|
|
135
|
+
- Have a hard timeout of **15 minutes** (configurable via the `SCRIPT_TIMEOUT_MS` constant)
|
|
136
|
+
|
|
137
|
+
The orchestrator exits with code `1` if any stage fails. Individual stage timeouts are also enforced per page via `--timeout-ms` (default: 30s).
|
|
138
|
+
|
|
139
|
+
If `node_modules/` is absent on first run, the orchestrator automatically installs dependencies via `pnpm install` (falls back to `npm install`).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# CLI Handbook
|
|
2
|
+
|
|
3
|
+
**Navigation**: [Home](../README.md) • [Architecture](architecture.md) • [CLI Handbook](cli-handbook.md) • [Output Artifacts](outputs.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Basic usage](#basic-usage)
|
|
10
|
+
- [Flag groups](#flag-groups)
|
|
11
|
+
- [Targeting & scope](#targeting--scope)
|
|
12
|
+
- [Audit intelligence](#audit-intelligence)
|
|
13
|
+
- [Execution & emulation](#execution--emulation)
|
|
14
|
+
- [Output generation](#output-generation)
|
|
15
|
+
- [Examples](#examples)
|
|
16
|
+
- [Exit codes](#exit-codes)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Basic usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx a11y-audit --base-url <url> [options]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or if installed locally:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node node_modules/@diegovelasquezweb/a11y-engine/scripts/audit.mjs --base-url <url> [options]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The only required flag is `--base-url`. All other flags are optional.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Flag groups
|
|
37
|
+
|
|
38
|
+
### Targeting & scope
|
|
39
|
+
|
|
40
|
+
Controls what gets scanned.
|
|
41
|
+
|
|
42
|
+
| Flag | Argument | Default | Description |
|
|
43
|
+
| :--- | :--- | :--- | :--- |
|
|
44
|
+
| `--base-url` | `<url>` | (Required) | Starting URL. Must include protocol (`https://` or `http://`). |
|
|
45
|
+
| `--max-routes` | `<num>` | `10` | Maximum unique same-origin paths to discover and scan. |
|
|
46
|
+
| `--crawl-depth` | `<num>` | `2` | How deep to follow links during BFS discovery (1–3). Has no effect when `--routes` is set. |
|
|
47
|
+
| `--routes` | `<csv>` | — | Explicit paths to scan (e.g. `/,/about,/contact`). Overrides auto-discovery entirely. |
|
|
48
|
+
| `--project-dir` | `<path>` | — | Path to the audited project source. Enables the source code pattern scanner and framework auto-detection from `package.json`. |
|
|
49
|
+
|
|
50
|
+
**Route discovery logic**:
|
|
51
|
+
1. If the target has a `sitemap.xml`, all listed URLs are used (up to `--max-routes`).
|
|
52
|
+
2. Otherwise, BFS crawl from `--base-url`, following same-origin `<a href>` links.
|
|
53
|
+
3. `--routes` always takes precedence over both.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### Audit intelligence
|
|
58
|
+
|
|
59
|
+
Controls how findings are interpreted and filtered.
|
|
60
|
+
|
|
61
|
+
| Flag | Argument | Default | Description |
|
|
62
|
+
| :--- | :--- | :--- | :--- |
|
|
63
|
+
| `--target` | `<text>` | `WCAG 2.2 AA` | Compliance target label rendered in reports. Does not change which rules run. |
|
|
64
|
+
| `--only-rule` | `<id>` | — | Run a single axe rule ID only. Useful for focused re-audits after fixing a specific issue. |
|
|
65
|
+
| `--ignore-findings` | `<csv>` | — | Comma-separated list of axe rule IDs to suppress from output entirely. |
|
|
66
|
+
| `--exclude-selectors` | `<csv>` | — | CSS selectors to skip. Elements matching these selectors are excluded from axe scanning. |
|
|
67
|
+
| `--framework` | `<name>` | — | Override auto-detected framework. Affects which fix notes and source boundaries are applied. |
|
|
68
|
+
|
|
69
|
+
**Supported `--framework` values**: `nextjs`, `gatsby`, `react`, `nuxt`, `vue`, `angular`, `astro`, `svelte`, `shopify`, `wordpress`, `drupal`.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### Execution & emulation
|
|
74
|
+
|
|
75
|
+
Controls browser behavior during scanning.
|
|
76
|
+
|
|
77
|
+
| Flag | Argument | Default | Description |
|
|
78
|
+
| :--- | :--- | :--- | :--- |
|
|
79
|
+
| `--color-scheme` | `light\|dark` | `light` | Emulates `prefers-color-scheme` media query. |
|
|
80
|
+
| `--wait-until` | `domcontentloaded\|load\|networkidle` | `domcontentloaded` | Playwright page load strategy. Use `networkidle` for SPAs with async rendering. |
|
|
81
|
+
| `--viewport` | `<WxH>` | `1280x800` | Browser viewport in pixels (e.g. `375x812` for mobile, `1440x900` for desktop). |
|
|
82
|
+
| `--wait-ms` | `<num>` | `2000` | Fixed delay (ms) after page load before axe runs. Useful when JS renders content after `DOMContentLoaded`. |
|
|
83
|
+
| `--timeout-ms` | `<num>` | `30000` | Network timeout per page load (ms). |
|
|
84
|
+
| `--headed` | — | `false` | Launch browser in visible mode. Useful for debugging page rendering issues. |
|
|
85
|
+
| `--affected-only` | — | `false` | Re-scan only routes that had violations in the previous scan. Reads `.audit/a11y-scan-results.json` to determine affected routes. Falls back to full scan if no prior results exist. |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Output generation
|
|
90
|
+
|
|
91
|
+
Controls what artifacts are written.
|
|
92
|
+
|
|
93
|
+
| Flag | Argument | Default | Description |
|
|
94
|
+
| :--- | :--- | :--- | :--- |
|
|
95
|
+
| `--with-reports` | — | `false` | Generate full artifact set: `report.html`, `report.pdf`, `checklist.html`, and `remediation.md`. Requires `--output`. |
|
|
96
|
+
| `--skip-reports` | — | `true` | Default behavior. Only `remediation.md` is generated. |
|
|
97
|
+
| `--output` | `<path>` | — | Absolute or relative path for `report.html`. PDF and checklist are derived from the same directory. |
|
|
98
|
+
| `--skip-patterns` | — | `false` | Disable source code pattern scanner even when `--project-dir` is set. Use this for DOM-only results without static analysis. |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Examples
|
|
103
|
+
|
|
104
|
+
### Minimal scan
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Produces only remediation.md in .audit/
|
|
108
|
+
a11y-audit --base-url https://example.com
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Full audit with all reports
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
a11y-audit \
|
|
115
|
+
--base-url https://example.com \
|
|
116
|
+
--with-reports \
|
|
117
|
+
--output ./audit/report.html
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Include source code intelligence
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
a11y-audit \
|
|
124
|
+
--base-url http://localhost:3000 \
|
|
125
|
+
--project-dir . \
|
|
126
|
+
--with-reports \
|
|
127
|
+
--output ./audit/report.html
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Focused re-audit — single rule, single route
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
a11y-audit \
|
|
134
|
+
--base-url https://example.com \
|
|
135
|
+
--only-rule color-contrast \
|
|
136
|
+
--routes /checkout \
|
|
137
|
+
--max-routes 1
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Fast re-audit after applying fixes
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Only re-scans routes that had violations in the last run
|
|
144
|
+
a11y-audit --base-url https://example.com --affected-only
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### SPA with deferred rendering
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
a11y-audit \
|
|
151
|
+
--base-url https://example.com \
|
|
152
|
+
--wait-until networkidle \
|
|
153
|
+
--wait-ms 3000
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Dark mode audit
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
a11y-audit --base-url https://example.com --color-scheme dark
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Mobile viewport
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
a11y-audit --base-url https://example.com --viewport 375x812
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Suppress known false positives
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
a11y-audit \
|
|
172
|
+
--base-url https://example.com \
|
|
173
|
+
--ignore-findings color-contrast,frame-title
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Explicit route list
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
a11y-audit \
|
|
180
|
+
--base-url https://example.com \
|
|
181
|
+
--routes /,/pricing,/blog,/contact
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Override framework detection
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
a11y-audit \
|
|
188
|
+
--base-url http://localhost:3000 \
|
|
189
|
+
--framework nextjs \
|
|
190
|
+
--project-dir .
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Exit codes
|
|
196
|
+
|
|
197
|
+
| Code | Meaning |
|
|
198
|
+
| :--- | :--- |
|
|
199
|
+
| `0` | Audit completed successfully (regardless of findings count) |
|
|
200
|
+
| `1` | Runtime error — invalid URL, missing required flag, stage failure, or timeout |
|
|
201
|
+
|
|
202
|
+
The engine never exits `1` just because findings were found. Exit `1` only indicates a pipeline or configuration error.
|
|
203
|
+
|
|
204
|
+
**Stdout markers** (parseable by scripts and CI):
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
REMEDIATION_PATH=<abs-path> # always printed on success
|
|
208
|
+
REPORT_PATH=<abs-path> # only printed when --with-reports is set
|
|
209
|
+
```
|
package/docs/outputs.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Output Artifacts
|
|
2
|
+
|
|
3
|
+
**Navigation**: [Home](../README.md) • [Architecture](architecture.md) • [CLI Handbook](cli-handbook.md) • [Output Artifacts](outputs.md)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Default output directory](#default-output-directory)
|
|
10
|
+
- [a11y-scan-results.json](#a11y-scan-resultsjson)
|
|
11
|
+
- [a11y-findings.json](#a11y-findingsjson)
|
|
12
|
+
- [remediation.md](#remediationmd)
|
|
13
|
+
- [report.html](#reporthtml)
|
|
14
|
+
- [report.pdf](#reportpdf)
|
|
15
|
+
- [checklist.html](#checklisthtml)
|
|
16
|
+
- [Consuming outputs programmatically](#consuming-outputs-programmatically)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Default output directory
|
|
21
|
+
|
|
22
|
+
All artifacts are written to `.audit/` relative to the package root (`SKILL_ROOT`). When consumed as an npm package, this resolves to the real package path inside `node_modules/`.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
.audit/
|
|
26
|
+
├── a11y-scan-results.json # raw axe-core scan data
|
|
27
|
+
├── a11y-findings.json # enriched findings (primary data artifact)
|
|
28
|
+
├── remediation.md # AI agent remediation guide
|
|
29
|
+
├── report.html # interactive dashboard (--with-reports)
|
|
30
|
+
├── report.pdf # compliance report (--with-reports)
|
|
31
|
+
├── checklist.html # manual testing checklist (--with-reports)
|
|
32
|
+
└── screenshots/ # element screenshots per violation
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
> When integrating the engine as a dependency (e.g. in `a11y-scanner`), use `fs.realpathSync` on the symlink path to resolve the real `.audit/` location — pnpm uses a deep `.pnpm/` directory structure, not the `node_modules/@scope/pkg` symlink.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## a11y-scan-results.json
|
|
40
|
+
|
|
41
|
+
Raw axe-core output per route. Written by `scripts/engine/dom-scanner.mjs`.
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"routes": [
|
|
46
|
+
{
|
|
47
|
+
"path": "/",
|
|
48
|
+
"url": "https://example.com/",
|
|
49
|
+
"violations": [...],
|
|
50
|
+
"incomplete": [...],
|
|
51
|
+
"passes": [...],
|
|
52
|
+
"inapplicable": [...]
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This file is consumed by `analyzer.mjs` and also used by `--affected-only` to determine which routes to re-scan on subsequent runs.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## a11y-findings.json
|
|
63
|
+
|
|
64
|
+
The primary enriched data artifact. Written by `scripts/engine/analyzer.mjs`. This is the file consumed by all report builders.
|
|
65
|
+
|
|
66
|
+
### Top-level structure
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"metadata": { ... },
|
|
71
|
+
"findings": [ ... ]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `metadata` fields
|
|
76
|
+
|
|
77
|
+
| Field | Type | Description |
|
|
78
|
+
| :--- | :--- | :--- |
|
|
79
|
+
| `scanDate` | `string` (ISO 8601) | Timestamp of when the scan ran |
|
|
80
|
+
| `checklist` | `object` | Manual check results if checklist was run |
|
|
81
|
+
| `projectContext` | `object` | Auto-detected framework, CMS, and UI libraries |
|
|
82
|
+
| `overallAssessment` | `string` | `"Pass"`, `"Conditional Pass"`, or `"Fail"` |
|
|
83
|
+
| `passedCriteria` | `string[]` | WCAG criterion IDs with no active violations |
|
|
84
|
+
| `outOfScope` | `object[]` | Findings excluded due to guardrails |
|
|
85
|
+
| `recommendations` | `object[]` | Grouped fix recommendations by component |
|
|
86
|
+
| `testingMethodology` | `object` | Scan scope and methodology summary |
|
|
87
|
+
| `fpFiltered` | `number` | Count of findings filtered as likely false positives |
|
|
88
|
+
| `deduplicatedCount` | `number` | Count of duplicate findings removed |
|
|
89
|
+
|
|
90
|
+
### `findings` — per-finding fields
|
|
91
|
+
|
|
92
|
+
| Field | Type | Description |
|
|
93
|
+
| :--- | :--- | :--- |
|
|
94
|
+
| `id` | `string` | Deterministic finding ID (e.g. `A11Y-001`) |
|
|
95
|
+
| `rule_id` | `string` | axe-core rule ID (e.g. `color-contrast`) |
|
|
96
|
+
| `title` | `string` | Human-readable finding title |
|
|
97
|
+
| `severity` | `string` | `Critical`, `Serious`, `Moderate`, or `Minor` |
|
|
98
|
+
| `wcag` | `string` | WCAG success criterion (e.g. `1.4.3`) |
|
|
99
|
+
| `wcag_criterion_id` | `string` | Full WCAG criterion ID (e.g. `1.4.3`) |
|
|
100
|
+
| `wcag_classification` | `string` | `A`, `AA`, `AAA`, or `Best Practice` |
|
|
101
|
+
| `area` | `string` | Affected page area (e.g. `Navigation`, `Forms`) |
|
|
102
|
+
| `url` | `string` | URL where the violation was found |
|
|
103
|
+
| `selector` | `string` | CSS selector for the affected element |
|
|
104
|
+
| `primary_selector` | `string` | Most stable selector chosen by the analyzer |
|
|
105
|
+
| `impacted_users` | `string` | Disability groups affected |
|
|
106
|
+
| `actual` | `string` | Observed violation description |
|
|
107
|
+
| `expected` | `string` | What the correct behavior should be |
|
|
108
|
+
| `category` | `string` | Violation category (e.g. `Color & Contrast`) |
|
|
109
|
+
| `primary_failure_mode` | `string\|null` | Root cause classification |
|
|
110
|
+
| `relationship_hint` | `string\|null` | Label/input relationship context |
|
|
111
|
+
| `failure_checks` | `object[]` | axe check-level failure details |
|
|
112
|
+
| `related_context` | `object[]` | Surrounding DOM context |
|
|
113
|
+
| `fix_description` | `string\|null` | Plain-language fix explanation |
|
|
114
|
+
| `fix_code` | `string\|null` | Ready-to-apply code snippet |
|
|
115
|
+
| `fix_code_lang` | `string` | Language for code block (e.g. `html`, `jsx`) |
|
|
116
|
+
| `recommended_fix` | `string` | Link to canonical fix reference (APG, MDN) |
|
|
117
|
+
| `mdn` | `string\|null` | MDN documentation URL |
|
|
118
|
+
| `effort` | `string\|null` | Fix effort estimate (`low`, `medium`, `high`) |
|
|
119
|
+
| `related_rules` | `string[]` | Related axe rule IDs |
|
|
120
|
+
| `guardrails` | `object\|null` | Agent scope guardrails for this finding |
|
|
121
|
+
| `false_positive_risk` | `string\|null` | Known false positive patterns |
|
|
122
|
+
| `fix_difficulty_notes` | `string\|null` | Edge cases and pitfalls for this fix |
|
|
123
|
+
| `framework_notes` | `string\|null` | Framework-specific fix guidance |
|
|
124
|
+
| `cms_notes` | `string\|null` | CMS-specific fix guidance |
|
|
125
|
+
| `check_data` | `object\|null` | Raw axe check data |
|
|
126
|
+
| `total_instances` | `number` | Count of affected elements across all pages |
|
|
127
|
+
| `evidence` | `object[]` | DOM HTML snippets for each affected element |
|
|
128
|
+
| `screenshot_path` | `string\|null` | Path to element screenshot |
|
|
129
|
+
| `file_search_pattern` | `string\|null` | Regex/glob pattern to find the source file |
|
|
130
|
+
| `managed_by_library` | `string\|null` | UI library managing this element (if any) |
|
|
131
|
+
| `ownership_status` | `string` | `confirmed`, `potential`, or `unknown` |
|
|
132
|
+
| `ownership_reason` | `string\|null` | Why this ownership status was assigned |
|
|
133
|
+
| `primary_source_scope` | `string[]` | Source file paths likely containing the issue |
|
|
134
|
+
| `search_strategy` | `string` | Agent search strategy recommendation |
|
|
135
|
+
| `component_hint` | `string\|null` | Component name hint for source search |
|
|
136
|
+
| `verification_command` | `string\|null` | CLI command to verify fix was applied |
|
|
137
|
+
| `verification_command_fallback` | `string\|null` | Fallback verify command |
|
|
138
|
+
| `pages_affected` | `number\|null` | Number of pages with this violation |
|
|
139
|
+
| `affected_urls` | `string[]\|null` | All URLs where this violation appears |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## remediation.md
|
|
144
|
+
|
|
145
|
+
AI agent-optimized remediation guide. Always generated (even without `--with-reports`). Written to `.audit/remediation.md`.
|
|
146
|
+
|
|
147
|
+
Content:
|
|
148
|
+
|
|
149
|
+
- Audit summary header with score, URL, and date
|
|
150
|
+
- Per-finding sections with: violation description, DOM evidence, fix description, code snippet, verify command, and WCAG criterion
|
|
151
|
+
- "Passed WCAG 2.2 Criteria" section listing clean criteria
|
|
152
|
+
- "Source Code Patterns" section (if source scanner ran)
|
|
153
|
+
- Component grouping table for batching fixes
|
|
154
|
+
|
|
155
|
+
The path is printed to stdout on completion as `REMEDIATION_PATH=<path>`.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## report.html
|
|
160
|
+
|
|
161
|
+
Interactive HTML dashboard. Generated only with `--with-reports --output <path>`.
|
|
162
|
+
|
|
163
|
+
Features:
|
|
164
|
+
|
|
165
|
+
- Severity-grouped findings with expandable detail cards
|
|
166
|
+
- DOM evidence with syntax-highlighted code
|
|
167
|
+
- Screenshot thumbnails per finding
|
|
168
|
+
- Search and filter by severity, category, and page
|
|
169
|
+
- Compliance score gauge with grade label
|
|
170
|
+
- Persona impact breakdown (visual, motor, cognitive, etc.)
|
|
171
|
+
- Quick wins section (Critical/Serious findings with ready code)
|
|
172
|
+
|
|
173
|
+
Written to the path specified by `--output`. The path is printed to stdout as `REPORT_PATH=<path>`.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## report.pdf
|
|
178
|
+
|
|
179
|
+
Formal PDF compliance report for stakeholders. Generated alongside `report.html` when `--with-reports` is set.
|
|
180
|
+
|
|
181
|
+
Sections:
|
|
182
|
+
|
|
183
|
+
1. Cover page with score, date, and target URL
|
|
184
|
+
2. Table of Contents
|
|
185
|
+
3. Executive Summary — score, overall assessment, finding counts by severity
|
|
186
|
+
4. Legal Risk Summary — applicable regulations based on score tier
|
|
187
|
+
5. Methodology — scan scope, tools, and WCAG version
|
|
188
|
+
6. Findings Breakdown — severity table and issue summary
|
|
189
|
+
7. Recommended Next Steps — dynamically generated from findings
|
|
190
|
+
|
|
191
|
+
Written to the same directory as `--output` with `.pdf` extension.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## checklist.html
|
|
196
|
+
|
|
197
|
+
Interactive manual testing checklist. Generated alongside `report.html` when `--with-reports` is set.
|
|
198
|
+
|
|
199
|
+
Contains the 41 WCAG 2.2 AA manual checks that automated tools cannot detect, including:
|
|
200
|
+
|
|
201
|
+
- Keyboard navigation and focus order
|
|
202
|
+
- Screen reader announcements
|
|
203
|
+
- Motion and animation
|
|
204
|
+
- Zoom and reflow (400%)
|
|
205
|
+
- Cognitive load and reading level
|
|
206
|
+
|
|
207
|
+
Each item is checkable and includes testing instructions. State is not persisted between sessions.
|
|
208
|
+
|
|
209
|
+
Written to the same directory as `--output` as `checklist.html`.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Consuming outputs programmatically
|
|
214
|
+
|
|
215
|
+
### Reading `a11y-findings.json` from an integration
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
import fs from "node:fs";
|
|
219
|
+
import path from "node:path";
|
|
220
|
+
import { createRequire } from "node:module";
|
|
221
|
+
|
|
222
|
+
// Resolve real path (handles pnpm symlinks)
|
|
223
|
+
const req = createRequire(import.meta.url);
|
|
224
|
+
const auditScript = req.resolve("@diegovelasquezweb/a11y-engine/scripts/audit.mjs");
|
|
225
|
+
const engineRoot = path.dirname(path.dirname(auditScript));
|
|
226
|
+
const findingsPath = path.join(engineRoot, ".audit", "a11y-findings.json");
|
|
227
|
+
|
|
228
|
+
const { findings, metadata } = JSON.parse(fs.readFileSync(findingsPath, "utf-8"));
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
> Note: `import.meta.url` may be mangled by bundlers (e.g. Next.js). In that case, use `fs.realpathSync` on the known symlink instead:
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
const symlinkBase = path.join(process.cwd(), "node_modules", "@diegovelasquezweb", "a11y-engine");
|
|
235
|
+
const engineRoot = fs.realpathSync(symlinkBase);
|
|
236
|
+
const findingsPath = path.join(engineRoot, ".audit", "a11y-findings.json");
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Parsing stdout markers
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
OUTPUT=$(node scripts/audit.mjs --base-url https://example.com --with-reports --output ./audit/report.html)
|
|
243
|
+
REMEDIATION_PATH=$(echo "$OUTPUT" | grep REMEDIATION_PATH | cut -d= -f2)
|
|
244
|
+
REPORT_PATH=$(echo "$OUTPUT" | grep REPORT_PATH | cut -d= -f2)
|
|
245
|
+
```
|
package/package.json
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegovelasquezweb/a11y-engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "WCAG 2.2 AA accessibility audit engine — scanner, analyzer, and report builders",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/diegovelasquezweb/a11y-engine.git"
|
|
9
|
+
"url": "git+https://github.com/diegovelasquezweb/a11y-engine.git"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/diegovelasquezweb/a11y-engine#readme",
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=18"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"a11y-audit": "
|
|
16
|
+
"a11y-audit": "scripts/audit.mjs"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"scripts/**",
|
|
20
20
|
"assets/**",
|
|
21
|
+
"docs/**",
|
|
21
22
|
"README.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
22
24
|
"LICENSE"
|
|
23
25
|
],
|
|
24
26
|
"scripts": {
|
|
@@ -33,5 +35,6 @@
|
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"vitest": "^4.0.18"
|
|
36
|
-
}
|
|
38
|
+
},
|
|
39
|
+
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
|
|
37
40
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { chromium } from "playwright";
|
|
10
10
|
import AxeBuilder from "@axe-core/playwright";
|
|
11
|
+
import pa11y from "pa11y";
|
|
11
12
|
import { log, DEFAULTS, writeJson, getInternalPath } from "../core/utils.mjs";
|
|
12
13
|
import { ASSET_PATHS, loadAssetJson } from "../core/asset-loader.mjs";
|
|
13
14
|
import path from "node:path";
|
|
@@ -85,6 +86,7 @@ function parseArgs(argv) {
|
|
|
85
86
|
onlyRule: null,
|
|
86
87
|
crawlDepth: DEFAULTS.crawlDepth,
|
|
87
88
|
viewport: null,
|
|
89
|
+
axeTags: null,
|
|
88
90
|
};
|
|
89
91
|
|
|
90
92
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -110,6 +112,7 @@ function parseArgs(argv) {
|
|
|
110
112
|
args.excludeSelectors = value.split(",").map((s) => s.trim());
|
|
111
113
|
if (key === "--color-scheme") args.colorScheme = value;
|
|
112
114
|
if (key === "--screenshots-dir") args.screenshotsDir = value;
|
|
115
|
+
if (key === "--axe-tags") args.axeTags = value.split(",").map((s) => s.trim());
|
|
113
116
|
if (key === "--viewport") {
|
|
114
117
|
const [w, h] = value.split("x").map(Number);
|
|
115
118
|
if (w && h) args.viewport = { width: w, height: h };
|
|
@@ -398,6 +401,7 @@ async function analyzeRoute(
|
|
|
398
401
|
timeoutMs = 30000,
|
|
399
402
|
maxRetries = 2,
|
|
400
403
|
waitUntil = "domcontentloaded",
|
|
404
|
+
axeTags = null,
|
|
401
405
|
) {
|
|
402
406
|
let lastError;
|
|
403
407
|
|
|
@@ -417,7 +421,8 @@ async function analyzeRoute(
|
|
|
417
421
|
log.info(`Targeted Audit: Only checking rule "${onlyRule}"`);
|
|
418
422
|
builder.withRules([onlyRule]);
|
|
419
423
|
} else {
|
|
420
|
-
|
|
424
|
+
const tagsToUse = axeTags || AXE_TAGS;
|
|
425
|
+
builder.withTags(tagsToUse);
|
|
421
426
|
}
|
|
422
427
|
|
|
423
428
|
if (Array.isArray(excludeSelectors)) {
|
|
@@ -470,6 +475,262 @@ async function analyzeRoute(
|
|
|
470
475
|
};
|
|
471
476
|
}
|
|
472
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Writes scan progress to a JSON file for real-time UI updates.
|
|
480
|
+
* @param {string} step - Current step identifier.
|
|
481
|
+
* @param {"pending"|"running"|"done"|"error"} status - Step status.
|
|
482
|
+
* @param {Object} [extra={}] - Additional metadata.
|
|
483
|
+
*/
|
|
484
|
+
function writeProgress(step, status, extra = {}) {
|
|
485
|
+
const progressPath = getInternalPath("progress.json");
|
|
486
|
+
let progress = {};
|
|
487
|
+
try {
|
|
488
|
+
if (fs.existsSync(progressPath)) {
|
|
489
|
+
progress = JSON.parse(fs.readFileSync(progressPath, "utf-8"));
|
|
490
|
+
}
|
|
491
|
+
} catch { /* ignore */ }
|
|
492
|
+
progress.steps = progress.steps || {};
|
|
493
|
+
progress.steps[step] = { status, updatedAt: new Date().toISOString(), ...extra };
|
|
494
|
+
progress.currentStep = step;
|
|
495
|
+
fs.mkdirSync(path.dirname(progressPath), { recursive: true });
|
|
496
|
+
fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Runs CDP (Chrome DevTools Protocol) accessibility checks using Playwright's CDP session.
|
|
501
|
+
* Catches issues axe-core may miss: missing accessible names, broken focus order,
|
|
502
|
+
* aria-hidden on focusable elements, and missing form labels.
|
|
503
|
+
* @param {import("playwright").Page} page - The Playwright page object.
|
|
504
|
+
* @returns {Promise<Object[]>} Array of CDP-sourced violations in axe-compatible format.
|
|
505
|
+
*/
|
|
506
|
+
async function runCdpChecks(page) {
|
|
507
|
+
const violations = [];
|
|
508
|
+
try {
|
|
509
|
+
const cdp = await page.context().newCDPSession(page);
|
|
510
|
+
|
|
511
|
+
const { nodes } = await cdp.send("Accessibility.getFullAXTree");
|
|
512
|
+
|
|
513
|
+
for (const node of nodes) {
|
|
514
|
+
const role = node.role?.value || "";
|
|
515
|
+
const name = node.name?.value || "";
|
|
516
|
+
const properties = node.properties || [];
|
|
517
|
+
const ignored = node.ignored || false;
|
|
518
|
+
|
|
519
|
+
if (ignored) continue;
|
|
520
|
+
|
|
521
|
+
const focusable = properties.find((p) => p.name === "focusable")?.value?.value === true;
|
|
522
|
+
const hidden = properties.find((p) => p.name === "hidden")?.value?.value === true;
|
|
523
|
+
|
|
524
|
+
const interactiveRoles = ["button", "link", "textbox", "combobox", "listbox", "menuitem", "tab", "checkbox", "radio", "switch", "slider"];
|
|
525
|
+
if (interactiveRoles.includes(role) && !name.trim()) {
|
|
526
|
+
const backendId = node.backendDOMNodeId;
|
|
527
|
+
let selector = "";
|
|
528
|
+
try {
|
|
529
|
+
if (backendId) {
|
|
530
|
+
const { object } = await cdp.send("DOM.resolveNode", { backendNodeId: backendId });
|
|
531
|
+
if (object?.objectId) {
|
|
532
|
+
const result = await cdp.send("Runtime.callFunctionOn", {
|
|
533
|
+
objectId: object.objectId,
|
|
534
|
+
functionDeclaration: `function() {
|
|
535
|
+
if (this.id) return '#' + this.id;
|
|
536
|
+
if (this.className && typeof this.className === 'string') return this.tagName.toLowerCase() + '.' + this.className.trim().split(/\\s+/).join('.');
|
|
537
|
+
return this.tagName.toLowerCase();
|
|
538
|
+
}`,
|
|
539
|
+
returnByValue: true,
|
|
540
|
+
});
|
|
541
|
+
selector = result.result?.value || "";
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} catch { /* fallback: no selector */ }
|
|
545
|
+
|
|
546
|
+
violations.push({
|
|
547
|
+
id: "cdp-missing-accessible-name",
|
|
548
|
+
impact: "serious",
|
|
549
|
+
tags: ["wcag2a", "wcag412", "cdp-check"],
|
|
550
|
+
description: `Interactive element with role "${role}" has no accessible name`,
|
|
551
|
+
help: "Interactive elements must have an accessible name",
|
|
552
|
+
helpUrl: "https://dequeuniversity.com/rules/axe/4.11/button-name",
|
|
553
|
+
source: "cdp",
|
|
554
|
+
nodes: [{
|
|
555
|
+
any: [],
|
|
556
|
+
all: [{
|
|
557
|
+
id: "cdp-accessible-name",
|
|
558
|
+
data: { role, name: "(empty)" },
|
|
559
|
+
relatedNodes: [],
|
|
560
|
+
impact: "serious",
|
|
561
|
+
message: `Element with role "${role}" has no accessible name in the accessibility tree`,
|
|
562
|
+
}],
|
|
563
|
+
none: [],
|
|
564
|
+
impact: "serious",
|
|
565
|
+
html: `<${role} aria-role="${role}">`,
|
|
566
|
+
target: selector ? [selector] : [`[role="${role}"]`],
|
|
567
|
+
failureSummary: `Fix all of the following:\n Element with role "${role}" has no accessible name`,
|
|
568
|
+
}],
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (hidden && focusable) {
|
|
573
|
+
violations.push({
|
|
574
|
+
id: "cdp-aria-hidden-focusable",
|
|
575
|
+
impact: "serious",
|
|
576
|
+
tags: ["wcag2a", "wcag412", "cdp-check"],
|
|
577
|
+
description: `Focusable element with role "${role}" is aria-hidden`,
|
|
578
|
+
help: "aria-hidden elements must not be focusable",
|
|
579
|
+
helpUrl: "https://dequeuniversity.com/rules/axe/4.11/aria-hidden-focus",
|
|
580
|
+
source: "cdp",
|
|
581
|
+
nodes: [{
|
|
582
|
+
any: [],
|
|
583
|
+
all: [{
|
|
584
|
+
id: "cdp-hidden-focusable",
|
|
585
|
+
data: { role },
|
|
586
|
+
relatedNodes: [],
|
|
587
|
+
impact: "serious",
|
|
588
|
+
message: `Focusable element with role "${role}" is hidden from the accessibility tree`,
|
|
589
|
+
}],
|
|
590
|
+
none: [],
|
|
591
|
+
impact: "serious",
|
|
592
|
+
html: `<element role="${role}" aria-hidden="true">`,
|
|
593
|
+
target: [`[role="${role}"]`],
|
|
594
|
+
failureSummary: `Fix all of the following:\n Focusable element is hidden from the accessibility tree`,
|
|
595
|
+
}],
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
await cdp.detach();
|
|
601
|
+
} catch (err) {
|
|
602
|
+
log.warn(`CDP checks failed (non-fatal): ${err.message}`);
|
|
603
|
+
}
|
|
604
|
+
return violations;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Runs pa11y (HTML CodeSniffer) against the already-loaded page URL.
|
|
609
|
+
* Catches WCAG violations that axe-core may miss, particularly around
|
|
610
|
+
* heading hierarchy, link purpose, and form associations.
|
|
611
|
+
* @param {string} routeUrl - The URL to scan.
|
|
612
|
+
* @param {string[]} [axeTags] - WCAG level tags for standard filtering.
|
|
613
|
+
* @returns {Promise<Object[]>} Array of pa11y-sourced violations in axe-compatible format.
|
|
614
|
+
*/
|
|
615
|
+
async function runPa11yChecks(routeUrl, axeTags) {
|
|
616
|
+
const violations = [];
|
|
617
|
+
try {
|
|
618
|
+
let standard = "WCAG2AA";
|
|
619
|
+
if (axeTags) {
|
|
620
|
+
if (axeTags.includes("wcag2aaa")) standard = "WCAG2AAA";
|
|
621
|
+
else if (axeTags.includes("wcag2aa") || axeTags.includes("wcag21aa") || axeTags.includes("wcag22aa")) standard = "WCAG2AA";
|
|
622
|
+
else if (axeTags.includes("wcag2a")) standard = "WCAG2A";
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const results = await pa11y(routeUrl, {
|
|
626
|
+
standard,
|
|
627
|
+
timeout: 30000,
|
|
628
|
+
wait: 2000,
|
|
629
|
+
chromeLaunchConfig: {
|
|
630
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
631
|
+
},
|
|
632
|
+
ignore: [
|
|
633
|
+
"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
|
|
634
|
+
"WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
|
|
635
|
+
],
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const impactMap = { 1: "serious", 2: "moderate", 3: "minor" };
|
|
639
|
+
|
|
640
|
+
for (const issue of results.issues || []) {
|
|
641
|
+
if (issue.type === "notice") continue;
|
|
642
|
+
|
|
643
|
+
const impact = impactMap[issue.typeCode] || "moderate";
|
|
644
|
+
|
|
645
|
+
let wcagCriterion = "";
|
|
646
|
+
const wcagMatch = issue.code?.match(/Guideline(\d+)_(\d+)\.(\d+)_(\d+)_(\d+)/);
|
|
647
|
+
if (wcagMatch) {
|
|
648
|
+
wcagCriterion = `${wcagMatch[3]}.${wcagMatch[4]}.${wcagMatch[5]}`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const ruleId = `pa11y-${(issue.code || "unknown").replace(/\./g, "-").toLowerCase().slice(0, 60)}`;
|
|
652
|
+
|
|
653
|
+
violations.push({
|
|
654
|
+
id: ruleId,
|
|
655
|
+
impact,
|
|
656
|
+
tags: ["pa11y-check", ...(wcagCriterion ? [`wcag${wcagCriterion.replace(/\./g, "")}`] : [])],
|
|
657
|
+
description: issue.message || "pa11y detected an accessibility issue",
|
|
658
|
+
help: issue.message?.split(".")[0] || "Accessibility issue detected by HTML CodeSniffer",
|
|
659
|
+
helpUrl: wcagCriterion
|
|
660
|
+
? `https://www.w3.org/WAI/WCAG21/Understanding/${wcagCriterion.replace(/\./g, "")}`
|
|
661
|
+
: "https://squizlabs.github.io/HTML_CodeSniffer/",
|
|
662
|
+
source: "pa11y",
|
|
663
|
+
nodes: [{
|
|
664
|
+
any: [],
|
|
665
|
+
all: [{
|
|
666
|
+
id: "pa11y-check",
|
|
667
|
+
data: { code: issue.code, context: issue.context?.slice(0, 200) },
|
|
668
|
+
relatedNodes: [],
|
|
669
|
+
impact,
|
|
670
|
+
message: issue.message || "",
|
|
671
|
+
}],
|
|
672
|
+
none: [],
|
|
673
|
+
impact,
|
|
674
|
+
html: issue.context || "",
|
|
675
|
+
target: issue.selector ? [issue.selector] : [],
|
|
676
|
+
failureSummary: `Fix all of the following:\n ${issue.message || "Accessibility issue"}`,
|
|
677
|
+
}],
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
log.warn(`pa11y checks failed (non-fatal): ${err.message}`);
|
|
682
|
+
}
|
|
683
|
+
return violations;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Merges violations from multiple sources (axe-core, CDP, pa11y) and deduplicates.
|
|
688
|
+
* Deduplication is based on rule ID + first target selector combination.
|
|
689
|
+
* @param {Object[]} axeViolations - Violations from axe-core.
|
|
690
|
+
* @param {Object[]} cdpViolations - Violations from CDP checks.
|
|
691
|
+
* @param {Object[]} pa11yViolations - Violations from pa11y.
|
|
692
|
+
* @returns {Object[]} Merged and deduplicated violations array.
|
|
693
|
+
*/
|
|
694
|
+
function mergeViolations(axeViolations, cdpViolations, pa11yViolations) {
|
|
695
|
+
const seen = new Set();
|
|
696
|
+
const merged = [];
|
|
697
|
+
|
|
698
|
+
for (const v of axeViolations) {
|
|
699
|
+
const key = `${v.id}::${v.nodes?.[0]?.target?.[0] || ""}`;
|
|
700
|
+
seen.add(key);
|
|
701
|
+
merged.push(v);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
for (const v of cdpViolations) {
|
|
705
|
+
const axeEquiv = {
|
|
706
|
+
"cdp-missing-accessible-name": ["button-name", "link-name", "input-name", "aria-command-name"],
|
|
707
|
+
"cdp-aria-hidden-focusable": ["aria-hidden-focus"],
|
|
708
|
+
};
|
|
709
|
+
const equivRules = axeEquiv[v.id] || [];
|
|
710
|
+
const target = v.nodes?.[0]?.target?.[0] || "";
|
|
711
|
+
const isDuplicate = equivRules.some((r) => seen.has(`${r}::${target}`));
|
|
712
|
+
if (!isDuplicate) {
|
|
713
|
+
const key = `${v.id}::${target}`;
|
|
714
|
+
if (!seen.has(key)) {
|
|
715
|
+
seen.add(key);
|
|
716
|
+
merged.push(v);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
for (const v of pa11yViolations) {
|
|
722
|
+
const target = v.nodes?.[0]?.target?.[0] || "";
|
|
723
|
+
const key = `${v.id}::${target}`;
|
|
724
|
+
const selectorCovered = [...seen].some((k) => k.endsWith(`::${target}`) && target);
|
|
725
|
+
if (!seen.has(key) && (!selectorCovered || !target)) {
|
|
726
|
+
seen.add(key);
|
|
727
|
+
merged.push(v);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return merged;
|
|
732
|
+
}
|
|
733
|
+
|
|
473
734
|
/**
|
|
474
735
|
* The main execution function for the accessibility scanner.
|
|
475
736
|
* Coordinates browser setup, crawling/discovery, parallel scanning, and result saving.
|
|
@@ -630,6 +891,8 @@ async function main() {
|
|
|
630
891
|
tabPages.push(await context.newPage());
|
|
631
892
|
}
|
|
632
893
|
|
|
894
|
+
writeProgress("page", "running");
|
|
895
|
+
|
|
633
896
|
for (let i = 0; i < routes.length; i += tabPages.length) {
|
|
634
897
|
const batch = [];
|
|
635
898
|
for (let j = 0; j < tabPages.length && i + j < routes.length; j++) {
|
|
@@ -640,6 +903,11 @@ async function main() {
|
|
|
640
903
|
const routePath = routes[idx];
|
|
641
904
|
log.info(`[${idx + 1}/${total}] Scanning: ${routePath}`);
|
|
642
905
|
const targetUrl = new URL(routePath, baseUrl).toString();
|
|
906
|
+
|
|
907
|
+
writeProgress("page", "done");
|
|
908
|
+
|
|
909
|
+
// Step 1: axe-core
|
|
910
|
+
writeProgress("axe", "running");
|
|
643
911
|
const result = await analyzeRoute(
|
|
644
912
|
tabPage,
|
|
645
913
|
targetUrl,
|
|
@@ -649,13 +917,51 @@ async function main() {
|
|
|
649
917
|
args.timeoutMs,
|
|
650
918
|
2,
|
|
651
919
|
args.waitUntil,
|
|
920
|
+
args.axeTags,
|
|
921
|
+
);
|
|
922
|
+
const axeViolationCount = result.violations?.length || 0;
|
|
923
|
+
writeProgress("axe", "done", { found: axeViolationCount });
|
|
924
|
+
log.info(`axe-core: ${axeViolationCount} violation(s) found`);
|
|
925
|
+
|
|
926
|
+
// Step 2: CDP checks
|
|
927
|
+
writeProgress("cdp", "running");
|
|
928
|
+
const cdpViolations = await runCdpChecks(tabPage);
|
|
929
|
+
writeProgress("cdp", "done", { found: cdpViolations.length });
|
|
930
|
+
log.info(`CDP checks: ${cdpViolations.length} issue(s) found`);
|
|
931
|
+
|
|
932
|
+
// Step 3: pa11y
|
|
933
|
+
writeProgress("pa11y", "running");
|
|
934
|
+
const pa11yViolations = await runPa11yChecks(targetUrl, args.axeTags);
|
|
935
|
+
writeProgress("pa11y", "done", { found: pa11yViolations.length });
|
|
936
|
+
log.info(`pa11y: ${pa11yViolations.length} issue(s) found`);
|
|
937
|
+
|
|
938
|
+
// Step 4: Merge results
|
|
939
|
+
writeProgress("merge", "running");
|
|
940
|
+
const mergedViolations = mergeViolations(
|
|
941
|
+
result.violations || [],
|
|
942
|
+
cdpViolations,
|
|
943
|
+
pa11yViolations,
|
|
652
944
|
);
|
|
653
|
-
|
|
654
|
-
|
|
945
|
+
writeProgress("merge", "done", {
|
|
946
|
+
axe: axeViolationCount,
|
|
947
|
+
cdp: cdpViolations.length,
|
|
948
|
+
pa11y: pa11yViolations.length,
|
|
949
|
+
merged: mergedViolations.length,
|
|
950
|
+
});
|
|
951
|
+
log.info(`Merged: ${mergedViolations.length} total unique violations (axe: ${axeViolationCount}, cdp: ${cdpViolations.length}, pa11y: ${pa11yViolations.length})`);
|
|
952
|
+
|
|
953
|
+
// Screenshots for merged violations
|
|
954
|
+
if (args.screenshotsDir && mergedViolations) {
|
|
955
|
+
for (const violation of mergedViolations) {
|
|
655
956
|
await captureElementScreenshot(tabPage, violation, idx);
|
|
656
957
|
}
|
|
657
958
|
}
|
|
658
|
-
results[idx] = {
|
|
959
|
+
results[idx] = {
|
|
960
|
+
path: routePath,
|
|
961
|
+
...result,
|
|
962
|
+
violations: mergedViolations,
|
|
963
|
+
incomplete: result.incomplete || [],
|
|
964
|
+
};
|
|
659
965
|
})(),
|
|
660
966
|
);
|
|
661
967
|
}
|