@a11y-skills/audit 0.3.0 → 0.4.0
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 +23 -0
- package/README.ja.md +64 -0
- package/README.md +64 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +407 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +99 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.js +1 -1
- package/dist/playwright/runAutoPlayDetection.js +7 -2
- package/dist/playwright/runAutocompleteAudit.js +7 -2
- package/dist/playwright/runFocusIndicatorCheck.d.ts +7 -0
- package/dist/playwright/runFocusIndicatorCheck.js +45 -21
- package/dist/playwright/runReflowCheck.js +4 -1
- package/dist/playwright/runTargetSizeCheck.js +8 -3
- package/dist/playwright/runTextSpacingCheck.js +14 -4
- package/dist/playwright/runTimeLimitDetector.js +21 -15
- package/dist/playwright/runZoomCheck.js +12 -5
- package/dist/schemas/index.js +11 -2
- package/dist/utils/axe-format.js +12 -3
- package/dist/utils/layout.js +7 -2
- package/dist/utils/recommendations.js +2 -2
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
All notable changes to `@a11y-skills/audit` are documented here. This project
|
|
4
4
|
adheres to [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 0.4.0 — 2026-06-12
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- `bin` CLI (`a11y-audit`): run all ten WCAG checks against a URL without a
|
|
11
|
+
Playwright test runner — `npx -y @a11y-skills/audit --url <url>`. Flags:
|
|
12
|
+
`--checks <list>`, `--output-dir`, `--screenshot`, `--list-checks`,
|
|
13
|
+
`--version`, `--help`. Exit codes: `0` = no violations, `1` = violations
|
|
14
|
+
found, `2` = runtime error. JSON output uses the same envelope as the
|
|
15
|
+
function API regardless of exit code.
|
|
16
|
+
- `test/fixtures/cli-smoke.html`: network-independent smoke fixture (a page
|
|
17
|
+
with intentional axe violations) for the CI CLI smoke test.
|
|
18
|
+
|
|
19
|
+
## 0.3.1
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `runFocusIndicatorCheck`: focus-triggered navigations slower than the
|
|
24
|
+
per-Tab settle window are no longer silently missed. The settle window is
|
|
25
|
+
now configurable via the new `navigationSettleMs` option (default: 50), and
|
|
26
|
+
a `framenavigated` listener additionally catches URL changes that commit and
|
|
27
|
+
revert within a single window. (#26)
|
|
28
|
+
|
|
6
29
|
## 0.3.0
|
|
7
30
|
|
|
8
31
|
**Breaking** — every check now returns (and saves) a single axe-style envelope
|
package/README.ja.md
CHANGED
|
@@ -50,6 +50,70 @@ npm install -D pixelmatch pngjs # runAutoPlayDetection を使う場合のみ
|
|
|
50
50
|
> ESM 専用です。CommonJS ビルドは同梱しません。ESM(または ESM 出力の TypeScript)から
|
|
51
51
|
> import してください。
|
|
52
52
|
|
|
53
|
+
## CLI
|
|
54
|
+
|
|
55
|
+
テストランナー不要で、コマンド 1 本で 10 検査すべてを実行できます。
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npx -y @a11y-skills/audit --url https://example.com
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
npm 7 以降では peer dependencies(`@playwright/test`、`@axe-core/playwright`)が
|
|
62
|
+
自動でインストールされます。初回は Chromium のインストールが必要です:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
npx playwright install chromium
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### フラグ
|
|
69
|
+
|
|
70
|
+
| フラグ | デフォルト | 説明 |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `--url <url>` | `TEST_PAGE` 環境変数 | 対象 URL。`--url` が環境変数より優先されます。どちらも未設定の場合は usage を表示して exit 2。 |
|
|
73
|
+
| `--checks <list>` | `all` | カンマ区切りのチェック名(下記一覧参照)。 |
|
|
74
|
+
| `--output-dir <dir>` | `./a11y-audit-results` | 結果 JSON の出力ディレクトリ。 |
|
|
75
|
+
| `--screenshot` | off | focus-indicator のスクリーンショットを有効化。 |
|
|
76
|
+
| `--list-checks` | — | チェック名一覧を出力して exit 0。 |
|
|
77
|
+
| `--version` | — | バージョンを出力して exit 0。 |
|
|
78
|
+
| `--help` | — | usage を出力して exit 0。 |
|
|
79
|
+
|
|
80
|
+
### チェック名一覧
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
axe-audit
|
|
84
|
+
focus-indicator-check
|
|
85
|
+
reflow-check
|
|
86
|
+
text-spacing-check
|
|
87
|
+
zoom-200-check
|
|
88
|
+
orientation-check
|
|
89
|
+
autocomplete-audit
|
|
90
|
+
time-limit-detector
|
|
91
|
+
auto-play-detection
|
|
92
|
+
target-size-check
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 終了コード
|
|
96
|
+
|
|
97
|
+
| コード | 意味 |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `0` | すべてのチェックが完了し、違反なし。 |
|
|
100
|
+
| `1` | すべてのチェックが完了し、1 件以上の違反あり。 |
|
|
101
|
+
| `2` | 実行時エラー — 引数不正・peer 欠落・ナビゲーション失敗・チェックのクラッシュ。 |
|
|
102
|
+
|
|
103
|
+
exit コードが非 0 でも、完了したチェックの JSON は必ず書き出されます。
|
|
104
|
+
|
|
105
|
+
### peer 要件
|
|
106
|
+
|
|
107
|
+
`@playwright/test >=1.50.0` および `@axe-core/playwright >=4.10.0` が必要です。
|
|
108
|
+
見つからない場合は exit 2 でインストール手順を案内します。
|
|
109
|
+
`--list-checks`、`--help`、`--version` は peer なしでも動作します。
|
|
110
|
+
|
|
111
|
+
### `auto-play-detection` と optional deps
|
|
112
|
+
|
|
113
|
+
`auto-play-detection` は `pixelmatch` と `pngjs` が必要です。
|
|
114
|
+
未インストールの場合はそのチェックだけ `SKIPPED` となり、終了コードには影響しません。
|
|
115
|
+
他の 9 検査は通常通り実行されます。
|
|
116
|
+
|
|
53
117
|
## 使い方 — 関数 API(推奨)
|
|
54
118
|
|
|
55
119
|
ページの遷移は呼び出し側で行い、関数は検査の実行・結果 JSON の書き出し・結果オブジェクトの
|
package/README.md
CHANGED
|
@@ -52,6 +52,70 @@ npm install -D pixelmatch pngjs # only if you use runAutoPlayDetection
|
|
|
52
52
|
> ESM only. This package does not ship a CommonJS build; import it from ESM
|
|
53
53
|
> (or a TypeScript project compiled to ESM).
|
|
54
54
|
|
|
55
|
+
## CLI
|
|
56
|
+
|
|
57
|
+
Run all ten checks against a URL with a single command — no test runner needed.
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npx -y @a11y-skills/audit --url https://example.com
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Peer dependencies (`@playwright/test`, `@axe-core/playwright`) are pulled in
|
|
64
|
+
automatically by npm 7+. Install Chromium on first use:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npx playwright install chromium
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Flags
|
|
71
|
+
|
|
72
|
+
| Flag | Default | Description |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| `--url <url>` | `TEST_PAGE` env | Target URL. `--url` takes priority over the env var. If neither is set, usage is printed and the process exits 2. |
|
|
75
|
+
| `--checks <list>` | `all` | Comma-separated check names (see list below). |
|
|
76
|
+
| `--output-dir <dir>` | `./a11y-audit-results` | Directory for result JSON files. |
|
|
77
|
+
| `--screenshot` | off | Enable focus-indicator screenshot. |
|
|
78
|
+
| `--list-checks` | — | Print check names and exit 0. |
|
|
79
|
+
| `--version` | — | Print version and exit 0. |
|
|
80
|
+
| `--help` | — | Print usage and exit 0. |
|
|
81
|
+
|
|
82
|
+
### Available check names
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
axe-audit
|
|
86
|
+
focus-indicator-check
|
|
87
|
+
reflow-check
|
|
88
|
+
text-spacing-check
|
|
89
|
+
zoom-200-check
|
|
90
|
+
orientation-check
|
|
91
|
+
autocomplete-audit
|
|
92
|
+
time-limit-detector
|
|
93
|
+
auto-play-detection
|
|
94
|
+
target-size-check
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Exit codes
|
|
98
|
+
|
|
99
|
+
| Code | Meaning |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `0` | All checks completed, no violations found. |
|
|
102
|
+
| `1` | All checks completed, at least one violation found. |
|
|
103
|
+
| `2` | Runtime error — bad arguments, missing peers, navigation failure, or check crash. |
|
|
104
|
+
|
|
105
|
+
Completed-check JSON is always written even when the exit code is non-zero.
|
|
106
|
+
|
|
107
|
+
### Peer requirements
|
|
108
|
+
|
|
109
|
+
`@playwright/test >=1.50.0` and `@axe-core/playwright >=4.10.0` must be
|
|
110
|
+
resolvable. If they are not found, the CLI exits 2 with an install hint.
|
|
111
|
+
`--list-checks`, `--help`, and `--version` work without peers.
|
|
112
|
+
|
|
113
|
+
### `auto-play-detection` and optional deps
|
|
114
|
+
|
|
115
|
+
`auto-play-detection` needs `pixelmatch` and `pngjs`. If they are absent, that
|
|
116
|
+
check is skipped with a `SKIPPED` message and does not affect the exit code.
|
|
117
|
+
The other nine checks continue normally.
|
|
118
|
+
|
|
55
119
|
## Usage — function API (recommended)
|
|
56
120
|
|
|
57
121
|
You navigate the page; the function runs the check, writes a result JSON, and
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @a11y-skills/audit CLI
|
|
4
|
+
*
|
|
5
|
+
* Runs WCAG 2.2 accessibility checks against a URL without needing a
|
|
6
|
+
* Playwright test runner. All 10 checks are available via --checks.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 — completed, no violations found across all checks
|
|
10
|
+
* 1 — completed, at least one violation found
|
|
11
|
+
* 2 — runtime error (bad args, missing peers, navigation failure, check crash)
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @a11y-skills/audit CLI
|
|
4
|
+
*
|
|
5
|
+
* Runs WCAG 2.2 accessibility checks against a URL without needing a
|
|
6
|
+
* Playwright test runner. All 10 checks are available via --checks.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 — completed, no violations found across all checks
|
|
10
|
+
* 1 — completed, at least one violation found
|
|
11
|
+
* 2 — runtime error (bad args, missing peers, navigation failure, check crash)
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import { parseArgs } from 'node:util';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Check registry
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Checks are registered with lazy imports to avoid top-level peer resolution.
|
|
20
|
+
const CHECK_REGISTRY = [
|
|
21
|
+
{
|
|
22
|
+
name: 'axe-audit',
|
|
23
|
+
kind: 'page',
|
|
24
|
+
async run({ page, outputDir }) {
|
|
25
|
+
const { runAxeAudit } = await import('./playwright/runAxeAudit.js');
|
|
26
|
+
return (await runAxeAudit({ page, outputDir }));
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'focus-indicator-check',
|
|
31
|
+
kind: 'browser',
|
|
32
|
+
async run({ browser, outputDir, screenshot, url }) {
|
|
33
|
+
const { runFocusIndicatorCheck } = await import('./playwright/runFocusIndicatorCheck.js');
|
|
34
|
+
return (await runFocusIndicatorCheck({
|
|
35
|
+
browser,
|
|
36
|
+
targetUrl: url,
|
|
37
|
+
outputDir,
|
|
38
|
+
screenshot,
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'reflow-check',
|
|
44
|
+
kind: 'page',
|
|
45
|
+
async run({ page, outputDir }) {
|
|
46
|
+
const { runReflowCheck } = await import('./playwright/runReflowCheck.js');
|
|
47
|
+
return (await runReflowCheck({ page, outputDir }));
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'text-spacing-check',
|
|
52
|
+
kind: 'page',
|
|
53
|
+
async run({ page, outputDir }) {
|
|
54
|
+
const { runTextSpacingCheck } = await import('./playwright/runTextSpacingCheck.js');
|
|
55
|
+
return (await runTextSpacingCheck({
|
|
56
|
+
page,
|
|
57
|
+
outputDir,
|
|
58
|
+
}));
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'zoom-200-check',
|
|
63
|
+
kind: 'page',
|
|
64
|
+
async run({ page, outputDir }) {
|
|
65
|
+
const { runZoomCheck } = await import('./playwright/runZoomCheck.js');
|
|
66
|
+
return (await runZoomCheck({ page, outputDir }));
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'orientation-check',
|
|
71
|
+
kind: 'page',
|
|
72
|
+
async run({ page, outputDir }) {
|
|
73
|
+
const { runOrientationCheck } = await import('./playwright/runOrientationCheck.js');
|
|
74
|
+
return (await runOrientationCheck({
|
|
75
|
+
page,
|
|
76
|
+
outputDir,
|
|
77
|
+
}));
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'autocomplete-audit',
|
|
82
|
+
kind: 'page',
|
|
83
|
+
async run({ page, outputDir }) {
|
|
84
|
+
const { runAutocompleteAudit } = await import('./playwright/runAutocompleteAudit.js');
|
|
85
|
+
return (await runAutocompleteAudit({
|
|
86
|
+
page,
|
|
87
|
+
outputDir,
|
|
88
|
+
}));
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'time-limit-detector',
|
|
93
|
+
kind: 'page',
|
|
94
|
+
async run({ page, outputDir }) {
|
|
95
|
+
const { runTimeLimitDetector } = await import('./playwright/runTimeLimitDetector.js');
|
|
96
|
+
return (await runTimeLimitDetector({
|
|
97
|
+
page,
|
|
98
|
+
outputDir,
|
|
99
|
+
}));
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'auto-play-detection',
|
|
104
|
+
kind: 'page',
|
|
105
|
+
async run({ page, outputDir }) {
|
|
106
|
+
const { runAutoPlayDetection } = await import('./playwright/runAutoPlayDetection.js');
|
|
107
|
+
return (await runAutoPlayDetection({
|
|
108
|
+
page,
|
|
109
|
+
outputDir: path.join(outputDir, 'auto-play-screenshots'),
|
|
110
|
+
}));
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'target-size-check',
|
|
115
|
+
kind: 'page',
|
|
116
|
+
async run({ page, outputDir }) {
|
|
117
|
+
const { runTargetSizeCheck } = await import('./playwright/runTargetSizeCheck.js');
|
|
118
|
+
return (await runTargetSizeCheck({
|
|
119
|
+
page,
|
|
120
|
+
outputDir,
|
|
121
|
+
}));
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Helpers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
function printUsage() {
|
|
129
|
+
process.stdout.write(`
|
|
130
|
+
Usage: a11y-audit --url <url> [options]
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
--url <url> Target URL to audit. Overrides TEST_PAGE env var.
|
|
134
|
+
--checks <list> Comma-separated check names (default: all).
|
|
135
|
+
Use --list-checks to see available names.
|
|
136
|
+
--output-dir <dir> Directory for result JSON files (default: ./a11y-audit-results).
|
|
137
|
+
--screenshot Enable focus-indicator screenshot (default: off).
|
|
138
|
+
--list-checks Print available check names and exit.
|
|
139
|
+
--version Print version and exit.
|
|
140
|
+
--help Print this message and exit.
|
|
141
|
+
|
|
142
|
+
Exit codes:
|
|
143
|
+
0 No violations found.
|
|
144
|
+
1 One or more violations found.
|
|
145
|
+
2 Runtime error (bad arguments, missing peers, navigation failure).
|
|
146
|
+
|
|
147
|
+
Peer requirements:
|
|
148
|
+
@playwright/test >=1.50.0
|
|
149
|
+
@axe-core/playwright >=4.10.0
|
|
150
|
+
|
|
151
|
+
Install if missing: npm i -D @playwright/test @axe-core/playwright
|
|
152
|
+
Install browsers: npx playwright install chromium
|
|
153
|
+
|
|
154
|
+
auto-play-detection requires optional deps (pixelmatch, pngjs). If they are
|
|
155
|
+
missing, that check is skipped with a SKIPPED notice.
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
/** Verify required peers are resolvable. Returns an error string or null. */
|
|
159
|
+
async function checkPeers() {
|
|
160
|
+
const missing = [];
|
|
161
|
+
for (const pkg of ['@playwright/test', '@axe-core/playwright']) {
|
|
162
|
+
try {
|
|
163
|
+
await import(pkg);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
missing.push(pkg);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (missing.length > 0) {
|
|
170
|
+
return (`Missing required peer dependencies: ${missing.join(', ')}\n` +
|
|
171
|
+
`Install them with: npm i -D @playwright/test @axe-core/playwright`);
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function formatDuration(ms) {
|
|
176
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Main
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
async function main() {
|
|
182
|
+
// Read package version from package.json
|
|
183
|
+
let pkgVersion = 'unknown';
|
|
184
|
+
try {
|
|
185
|
+
const pkgPath = new URL('../package.json', import.meta.url);
|
|
186
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
187
|
+
pkgVersion = pkgJson.version;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// ignore
|
|
191
|
+
}
|
|
192
|
+
let values;
|
|
193
|
+
try {
|
|
194
|
+
const result = parseArgs({
|
|
195
|
+
args: process.argv.slice(2),
|
|
196
|
+
options: {
|
|
197
|
+
url: { type: 'string' },
|
|
198
|
+
checks: { type: 'string' },
|
|
199
|
+
'output-dir': { type: 'string' },
|
|
200
|
+
screenshot: { type: 'boolean', default: false },
|
|
201
|
+
'list-checks': { type: 'boolean', default: false },
|
|
202
|
+
version: { type: 'boolean', default: false },
|
|
203
|
+
help: { type: 'boolean', default: false },
|
|
204
|
+
},
|
|
205
|
+
strict: true,
|
|
206
|
+
});
|
|
207
|
+
values = result.values;
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
211
|
+
printUsage();
|
|
212
|
+
process.exit(2);
|
|
213
|
+
}
|
|
214
|
+
if (values.help) {
|
|
215
|
+
printUsage();
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
if (values.version) {
|
|
219
|
+
process.stdout.write(`${pkgVersion}\n`);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
if (values['list-checks']) {
|
|
223
|
+
process.stdout.write(CHECK_REGISTRY.map((c) => c.name).join('\n') + '\n');
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
// --- Resolve URL ---
|
|
227
|
+
const url = values.url ?? process.env.TEST_PAGE;
|
|
228
|
+
if (!url) {
|
|
229
|
+
process.stderr.write('Error: --url is required (or set the TEST_PAGE environment variable).\n');
|
|
230
|
+
printUsage();
|
|
231
|
+
process.exit(2);
|
|
232
|
+
}
|
|
233
|
+
// --- Resolve checks ---
|
|
234
|
+
const checksArg = values.checks;
|
|
235
|
+
let selectedChecks;
|
|
236
|
+
if (!checksArg || checksArg === 'all') {
|
|
237
|
+
selectedChecks = CHECK_REGISTRY;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const names = checksArg.split(',').map((n) => n.trim());
|
|
241
|
+
const unknown = names.filter((n) => !CHECK_REGISTRY.some((c) => c.name === n));
|
|
242
|
+
if (unknown.length > 0) {
|
|
243
|
+
process.stderr.write(`Error: unknown check(s): ${unknown.join(', ')}\n` +
|
|
244
|
+
`Run --list-checks to see available names.\n`);
|
|
245
|
+
process.exit(2);
|
|
246
|
+
}
|
|
247
|
+
selectedChecks = names
|
|
248
|
+
.map((n) => CHECK_REGISTRY.find((c) => c.name === n))
|
|
249
|
+
.filter((c) => c !== undefined);
|
|
250
|
+
}
|
|
251
|
+
const outputDir = path.resolve(values['output-dir'] ?? './a11y-audit-results');
|
|
252
|
+
const screenshot = values.screenshot === true;
|
|
253
|
+
// --- Peer check (only before running checks) ---
|
|
254
|
+
const peerError = await checkPeers();
|
|
255
|
+
if (peerError) {
|
|
256
|
+
process.stderr.write(`Error: ${peerError}\n`);
|
|
257
|
+
process.exit(2);
|
|
258
|
+
}
|
|
259
|
+
// --- Launch browser ---
|
|
260
|
+
let browser;
|
|
261
|
+
try {
|
|
262
|
+
const { chromium } = await import('@playwright/test');
|
|
263
|
+
browser = await chromium.launch();
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
process.stderr.write(`Error launching browser: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
267
|
+
process.exit(2);
|
|
268
|
+
}
|
|
269
|
+
// Ensure output directory exists
|
|
270
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
271
|
+
process.stdout.write(`a11y-audit v${pkgVersion}\n`);
|
|
272
|
+
process.stdout.write(`URL: ${url}\n`);
|
|
273
|
+
process.stdout.write(`Checks: ${selectedChecks.map((c) => c.name).join(', ')}\n`);
|
|
274
|
+
process.stdout.write(`Output dir: ${outputDir}\n`);
|
|
275
|
+
process.stdout.write(`\n`);
|
|
276
|
+
const summaries = [];
|
|
277
|
+
let hasViolations = false;
|
|
278
|
+
let hasErrors = false;
|
|
279
|
+
for (const check of selectedChecks) {
|
|
280
|
+
const start = Date.now();
|
|
281
|
+
// page-based checks get a fresh context + navigated page
|
|
282
|
+
let context = null;
|
|
283
|
+
let page = null;
|
|
284
|
+
try {
|
|
285
|
+
if (check.kind === 'page') {
|
|
286
|
+
context = await browser.newContext();
|
|
287
|
+
page = await context.newPage();
|
|
288
|
+
// Use 'load' for file: URLs to avoid networkidle hanging
|
|
289
|
+
const waitUntil = url.startsWith('file:') ? 'load' : 'networkidle';
|
|
290
|
+
try {
|
|
291
|
+
await page.goto(url, { waitUntil });
|
|
292
|
+
}
|
|
293
|
+
catch (navErr) {
|
|
294
|
+
const msg = navErr instanceof Error ? navErr.message : String(navErr);
|
|
295
|
+
process.stderr.write(`[${check.name}] Navigation failed: ${msg}\n`);
|
|
296
|
+
summaries.push({
|
|
297
|
+
name: check.name,
|
|
298
|
+
status: 'ERROR',
|
|
299
|
+
durationMs: Date.now() - start,
|
|
300
|
+
error: msg,
|
|
301
|
+
});
|
|
302
|
+
hasErrors = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const result = await check.run({
|
|
307
|
+
// page is null only for browser-kind checks, which do not use it
|
|
308
|
+
page: page,
|
|
309
|
+
browser,
|
|
310
|
+
outputDir,
|
|
311
|
+
screenshot,
|
|
312
|
+
url,
|
|
313
|
+
});
|
|
314
|
+
const durationMs = Date.now() - start;
|
|
315
|
+
const summary = result.summary;
|
|
316
|
+
const violationCount = summary?.violationCount ?? 0;
|
|
317
|
+
const incompleteCount = summary?.incompleteCount ?? 0;
|
|
318
|
+
const passCount = summary?.passCount ?? 0;
|
|
319
|
+
if (violationCount > 0) {
|
|
320
|
+
hasViolations = true;
|
|
321
|
+
}
|
|
322
|
+
process.stdout.write(`[${check.name}] DONE in ${formatDuration(durationMs)} — ` +
|
|
323
|
+
`violations: ${violationCount}, passes: ${passCount}, incomplete: ${incompleteCount}\n`);
|
|
324
|
+
summaries.push({
|
|
325
|
+
name: check.name,
|
|
326
|
+
status: 'DONE',
|
|
327
|
+
durationMs,
|
|
328
|
+
violationCount,
|
|
329
|
+
incompleteCount,
|
|
330
|
+
passCount,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
const durationMs = Date.now() - start;
|
|
335
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
336
|
+
// auto-play-detection: optional dep missing → SKIPPED, not ERROR
|
|
337
|
+
const isOptionalDepMissing = check.name === 'auto-play-detection' &&
|
|
338
|
+
(msg.includes('pixelmatch') ||
|
|
339
|
+
msg.includes('pngjs') ||
|
|
340
|
+
msg.includes('Cannot find module') ||
|
|
341
|
+
msg.includes('ERR_MODULE_NOT_FOUND'));
|
|
342
|
+
if (isOptionalDepMissing) {
|
|
343
|
+
process.stdout.write(`[${check.name}] SKIPPED — optional deps missing (pixelmatch, pngjs). ` +
|
|
344
|
+
`Install with: npm i pixelmatch pngjs\n`);
|
|
345
|
+
summaries.push({
|
|
346
|
+
name: check.name,
|
|
347
|
+
status: 'SKIPPED',
|
|
348
|
+
durationMs,
|
|
349
|
+
skipReason: 'optional deps missing (pixelmatch, pngjs)',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
process.stderr.write(`[${check.name}] ERROR: ${msg}\n`);
|
|
354
|
+
summaries.push({
|
|
355
|
+
name: check.name,
|
|
356
|
+
status: 'ERROR',
|
|
357
|
+
durationMs,
|
|
358
|
+
error: msg,
|
|
359
|
+
});
|
|
360
|
+
hasErrors = true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (context !== null) {
|
|
365
|
+
try {
|
|
366
|
+
await context.close();
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// ignore cleanup errors
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// --- Close browser ---
|
|
375
|
+
try {
|
|
376
|
+
await browser.close();
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// ignore
|
|
380
|
+
}
|
|
381
|
+
// --- Summary ---
|
|
382
|
+
const totalMs = summaries.reduce((acc, s) => acc + s.durationMs, 0);
|
|
383
|
+
const doneCount = summaries.filter((s) => s.status === 'DONE').length;
|
|
384
|
+
const skippedCount = summaries.filter((s) => s.status === 'SKIPPED').length;
|
|
385
|
+
const errorCount = summaries.filter((s) => s.status === 'ERROR').length;
|
|
386
|
+
const totalViolations = summaries.reduce((acc, s) => acc + (s.violationCount ?? 0), 0);
|
|
387
|
+
process.stdout.write(`\n`);
|
|
388
|
+
process.stdout.write(`--- Summary (${formatDuration(totalMs)}) ---\n` +
|
|
389
|
+
` Checks run: ${doneCount}\n` +
|
|
390
|
+
` Skipped: ${skippedCount}\n` +
|
|
391
|
+
` Errors: ${errorCount}\n` +
|
|
392
|
+
` Total violations: ${totalViolations}\n` +
|
|
393
|
+
` Results written to: ${outputDir}\n`);
|
|
394
|
+
if (hasErrors) {
|
|
395
|
+
process.exit(2);
|
|
396
|
+
}
|
|
397
|
+
else if (hasViolations) {
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
main().catch((err) => {
|
|
405
|
+
process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
406
|
+
process.exit(2);
|
|
407
|
+
});
|
package/dist/constants.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare const FOCUS_STYLE_PROPERTIES: readonly ["outline", "outlineStyle"
|
|
|
24
24
|
export declare const FOCUSABLE_SELECTOR: string;
|
|
25
25
|
/** Extra tab iterations for safety margin */
|
|
26
26
|
export declare const EXTRA_TAB_ITERATIONS = 10;
|
|
27
|
+
/** Default ms to wait after each Tab press for a focus-triggered navigation to surface. */
|
|
28
|
+
export declare const DEFAULT_NAVIGATION_SETTLE_MS = 50;
|
|
27
29
|
/**
|
|
28
30
|
* Minimum overlap ratio to report as obscured (0-1)
|
|
29
31
|
* 0.2 means 20% of the focused element must be covered
|