@a11y-skills/audit 0.2.0 → 0.3.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 +44 -0
- package/README.ja.md +52 -4
- package/README.md +53 -4
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/playwright/runAutoPlayDetection.js +8 -2
- package/dist/playwright/runAutocompleteAudit.js +40 -10
- package/dist/playwright/runAxeAudit.d.ts +4 -0
- package/dist/playwright/runAxeAudit.js +26 -30
- package/dist/playwright/runFocusIndicatorCheck.js +55 -12
- package/dist/playwright/runOrientationCheck.js +13 -7
- package/dist/playwright/runReflowCheck.js +18 -11
- package/dist/playwright/runTargetSizeCheck.js +42 -10
- package/dist/playwright/runTextSpacingCheck.js +52 -8
- package/dist/playwright/runTimeLimitDetector.js +36 -11
- package/dist/playwright/runZoomCheck.js +35 -11
- package/dist/schemas/index.d.ts +8 -1
- package/dist/schemas/index.js +388 -292
- package/dist/types.d.ts +137 -53
- package/dist/types.js +9 -0
- package/dist/utils/axe-format.d.ts +88 -0
- package/dist/utils/axe-format.js +361 -0
- package/dist/utils/layout.d.ts +2 -0
- package/dist/utils/layout.js +20 -1
- package/dist/utils/rule-registry.d.ts +216 -0
- package/dist/utils/rule-registry.js +220 -0
- package/dist/utils/test-harness.d.ts +2 -2
- package/dist/utils/test-harness.js +5 -6
- package/package.json +2 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule metadata registry for the custom (non-axe) checks.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for rule ids, WCAG mapping, severity, and the
|
|
5
|
+
* violation/incomplete classification. The mappers in `axe-format.ts` read
|
|
6
|
+
* from here so that classification policy is reviewable in one place.
|
|
7
|
+
*
|
|
8
|
+
* Classification policy: `'violation'` is reserved for rules whose detection
|
|
9
|
+
* has no blind spot AND where no WCAG exception can apply to a finding.
|
|
10
|
+
* Heuristic detections and findings with possible exceptions are
|
|
11
|
+
* `'incomplete'` (the manual-review queue). impact is NOT derived from the
|
|
12
|
+
* WCAG conformance level; it is assigned per rule, conservatively defaulting
|
|
13
|
+
* to `moderate`.
|
|
14
|
+
*/
|
|
15
|
+
const UNDERSTANDING = 'https://www.w3.org/WAI/WCAG22/Understanding';
|
|
16
|
+
export const RULES = {
|
|
17
|
+
// --- focus-indicator-check ---
|
|
18
|
+
'focus-visible': {
|
|
19
|
+
id: 'a11y-skills/focus-visible',
|
|
20
|
+
sc: ['2.4.7'],
|
|
21
|
+
tags: ['a11y-skills', 'wcag2aa', 'wcag247'],
|
|
22
|
+
impact: 'serious',
|
|
23
|
+
scope: 'node',
|
|
24
|
+
description: 'Ensure keyboard focus produces a visible indicator on focusable elements',
|
|
25
|
+
help: 'Focusable elements should have a visible focus indicator',
|
|
26
|
+
helpUrl: `${UNDERSTANDING}/focus-visible.html`,
|
|
27
|
+
// computed-style diffing cannot see pseudo-elements, canvas drawing, or
|
|
28
|
+
// parent-element changes, so a "no style change" finding is not proof.
|
|
29
|
+
classification: 'incomplete',
|
|
30
|
+
},
|
|
31
|
+
'no-context-change-on-focus': {
|
|
32
|
+
id: 'a11y-skills/no-context-change-on-focus',
|
|
33
|
+
sc: ['3.2.1'],
|
|
34
|
+
tags: ['a11y-skills', 'wcag2a', 'wcag321'],
|
|
35
|
+
impact: 'serious',
|
|
36
|
+
scope: 'node',
|
|
37
|
+
description: 'Ensure receiving focus does not trigger a change of context (navigation, new window)',
|
|
38
|
+
help: 'Focusing an element must not trigger a context change',
|
|
39
|
+
helpUrl: `${UNDERSTANDING}/on-focus.html`,
|
|
40
|
+
// the navigation is directly observed, no exception applies.
|
|
41
|
+
classification: 'violation',
|
|
42
|
+
},
|
|
43
|
+
'focus-not-obscured': {
|
|
44
|
+
id: 'a11y-skills/focus-not-obscured',
|
|
45
|
+
sc: ['2.4.11', '2.4.12'],
|
|
46
|
+
tags: ['a11y-skills', 'wcag22aa', 'wcag2411'],
|
|
47
|
+
impact: 'moderate',
|
|
48
|
+
scope: 'node',
|
|
49
|
+
description: 'Ensure the focused element is not hidden by fixed or sticky content',
|
|
50
|
+
help: 'Focused elements should not be obscured by author-created content',
|
|
51
|
+
helpUrl: `${UNDERSTANDING}/focus-not-obscured-minimum.html`,
|
|
52
|
+
classification: 'incomplete',
|
|
53
|
+
},
|
|
54
|
+
// --- reflow-check ---
|
|
55
|
+
'reflow-overflow': {
|
|
56
|
+
id: 'a11y-skills/reflow-overflow',
|
|
57
|
+
sc: ['1.4.10'],
|
|
58
|
+
tags: ['a11y-skills', 'wcag21aa', 'wcag1410'],
|
|
59
|
+
impact: 'serious',
|
|
60
|
+
scope: 'node',
|
|
61
|
+
description: 'Ensure content reflows at 320 CSS px width without horizontal scrolling',
|
|
62
|
+
help: 'Content should not require horizontal scrolling at 320px width',
|
|
63
|
+
helpUrl: `${UNDERSTANDING}/reflow.html`,
|
|
64
|
+
// two-dimensional layout exceptions (tables, maps, ...) need human judgment.
|
|
65
|
+
classification: 'incomplete',
|
|
66
|
+
},
|
|
67
|
+
'reflow-clipped-text': {
|
|
68
|
+
id: 'a11y-skills/reflow-clipped-text',
|
|
69
|
+
sc: ['1.4.10'],
|
|
70
|
+
tags: ['a11y-skills', 'wcag21aa', 'wcag1410'],
|
|
71
|
+
impact: 'moderate',
|
|
72
|
+
scope: 'node',
|
|
73
|
+
description: 'Ensure text is not clipped when content reflows at 320 CSS px',
|
|
74
|
+
help: 'Text should remain readable at 320px width',
|
|
75
|
+
helpUrl: `${UNDERSTANDING}/reflow.html`,
|
|
76
|
+
classification: 'incomplete',
|
|
77
|
+
},
|
|
78
|
+
// --- target-size-check ---
|
|
79
|
+
'target-size-minimum': {
|
|
80
|
+
id: 'a11y-skills/target-size-minimum',
|
|
81
|
+
sc: ['2.5.8'],
|
|
82
|
+
tags: ['a11y-skills', 'wcag22aa', 'wcag258'],
|
|
83
|
+
impact: 'serious',
|
|
84
|
+
scope: 'node',
|
|
85
|
+
description: 'Ensure pointer targets are at least 24x24 CSS px (WCAG 2.5.8 AA)',
|
|
86
|
+
help: 'Pointer targets should be at least 24x24 CSS px',
|
|
87
|
+
helpUrl: `${UNDERSTANDING}/target-size-minimum.html`,
|
|
88
|
+
// exception heuristics cannot rule out the essential exception; only
|
|
89
|
+
// nodes with exceptionAssessment 'ruled-out' are promoted to violations.
|
|
90
|
+
classification: 'incomplete',
|
|
91
|
+
},
|
|
92
|
+
'target-size-enhanced': {
|
|
93
|
+
id: 'a11y-skills/target-size-enhanced',
|
|
94
|
+
sc: ['2.5.5'],
|
|
95
|
+
tags: ['a11y-skills', 'wcag21aaa', 'wcag255'],
|
|
96
|
+
impact: 'moderate',
|
|
97
|
+
scope: 'node',
|
|
98
|
+
description: 'Ensure pointer targets are at least 44x44 CSS px (WCAG 2.5.5 AAA)',
|
|
99
|
+
help: 'Pointer targets should be at least 44x44 CSS px',
|
|
100
|
+
helpUrl: `${UNDERSTANDING}/target-size-enhanced.html`,
|
|
101
|
+
classification: 'incomplete',
|
|
102
|
+
},
|
|
103
|
+
// --- text-spacing-check ---
|
|
104
|
+
'text-spacing': {
|
|
105
|
+
id: 'a11y-skills/text-spacing',
|
|
106
|
+
sc: ['1.4.12'],
|
|
107
|
+
tags: ['a11y-skills', 'wcag21aa', 'wcag1412'],
|
|
108
|
+
impact: 'moderate',
|
|
109
|
+
scope: 'node',
|
|
110
|
+
description: 'Ensure no loss of content when WCAG 1.4.12 text spacing overrides are applied',
|
|
111
|
+
help: 'Text must not be clipped under increased text spacing',
|
|
112
|
+
helpUrl: `${UNDERSTANDING}/text-spacing.html`,
|
|
113
|
+
// applying the spacing values and observing clipping is the SC's own
|
|
114
|
+
// mechanical test procedure.
|
|
115
|
+
classification: 'violation',
|
|
116
|
+
},
|
|
117
|
+
// --- zoom-200-check ---
|
|
118
|
+
'resize-text': {
|
|
119
|
+
id: 'a11y-skills/resize-text',
|
|
120
|
+
sc: ['1.4.4'],
|
|
121
|
+
tags: ['a11y-skills', 'wcag2aa', 'wcag144'],
|
|
122
|
+
impact: 'moderate',
|
|
123
|
+
scope: 'node',
|
|
124
|
+
description: 'Ensure content remains usable when text is resized to 200%',
|
|
125
|
+
help: 'Content should not be lost or clipped at 200% zoom',
|
|
126
|
+
helpUrl: `${UNDERSTANDING}/resize-text.html`,
|
|
127
|
+
// horizontal scrolling at zoom does not by itself fail SC 1.4.4.
|
|
128
|
+
classification: 'incomplete',
|
|
129
|
+
},
|
|
130
|
+
// --- orientation-check ---
|
|
131
|
+
'orientation-lock': {
|
|
132
|
+
id: 'a11y-skills/orientation-lock',
|
|
133
|
+
sc: ['1.3.4'],
|
|
134
|
+
tags: ['a11y-skills', 'wcag21aa', 'wcag134'],
|
|
135
|
+
impact: 'serious',
|
|
136
|
+
scope: 'page',
|
|
137
|
+
description: 'Ensure content does not restrict its view to a single display orientation',
|
|
138
|
+
help: 'Content should work in both portrait and landscape orientation',
|
|
139
|
+
helpUrl: `${UNDERSTANDING}/orientation.html`,
|
|
140
|
+
// the essential exception requires human judgment.
|
|
141
|
+
classification: 'incomplete',
|
|
142
|
+
},
|
|
143
|
+
// --- autocomplete-audit ---
|
|
144
|
+
'autocomplete-invalid': {
|
|
145
|
+
id: 'a11y-skills/autocomplete-invalid',
|
|
146
|
+
sc: ['1.3.5'],
|
|
147
|
+
tags: ['a11y-skills', 'wcag21aa', 'wcag135'],
|
|
148
|
+
impact: 'moderate',
|
|
149
|
+
scope: 'node',
|
|
150
|
+
description: 'Ensure autocomplete attribute values are valid tokens',
|
|
151
|
+
help: 'autocomplete attributes must use valid token values',
|
|
152
|
+
helpUrl: `${UNDERSTANDING}/identify-input-purpose.html`,
|
|
153
|
+
// syntactic validity is machine-checkable.
|
|
154
|
+
classification: 'violation',
|
|
155
|
+
},
|
|
156
|
+
'autocomplete-missing': {
|
|
157
|
+
id: 'a11y-skills/autocomplete-missing',
|
|
158
|
+
sc: ['1.3.5'],
|
|
159
|
+
tags: ['a11y-skills', 'wcag21aa', 'wcag135'],
|
|
160
|
+
impact: 'moderate',
|
|
161
|
+
scope: 'node',
|
|
162
|
+
description: 'Ensure fields collecting user information declare their purpose via autocomplete',
|
|
163
|
+
help: 'Add an autocomplete attribute matching the field purpose',
|
|
164
|
+
helpUrl: `${UNDERSTANDING}/identify-input-purpose.html`,
|
|
165
|
+
// field purpose is inferred from name/id/label patterns — heuristic.
|
|
166
|
+
classification: 'incomplete',
|
|
167
|
+
},
|
|
168
|
+
// --- time-limit-detector ---
|
|
169
|
+
'meta-refresh': {
|
|
170
|
+
id: 'a11y-skills/meta-refresh',
|
|
171
|
+
sc: ['2.2.1'],
|
|
172
|
+
tags: ['a11y-skills', 'wcag2a', 'wcag221'],
|
|
173
|
+
impact: 'serious',
|
|
174
|
+
scope: 'node',
|
|
175
|
+
description: 'Ensure meta refresh time limits can be turned off, adjusted, or extended',
|
|
176
|
+
help: 'Verify the meta refresh satisfies an SC 2.2.1 exception or is adjustable',
|
|
177
|
+
helpUrl: `${UNDERSTANDING}/timing-adjustable.html`,
|
|
178
|
+
// adjustability / 20-hour exception cannot be determined automatically.
|
|
179
|
+
classification: 'incomplete',
|
|
180
|
+
},
|
|
181
|
+
'time-limit-timer': {
|
|
182
|
+
id: 'a11y-skills/time-limit-timer',
|
|
183
|
+
sc: ['2.2.1'],
|
|
184
|
+
tags: ['a11y-skills', 'wcag2a', 'wcag221'],
|
|
185
|
+
impact: 'moderate',
|
|
186
|
+
scope: 'page',
|
|
187
|
+
description: 'Detect JavaScript timers that may implement a time limit',
|
|
188
|
+
help: 'Verify whether detected timers implement an adjustable time limit',
|
|
189
|
+
helpUrl: `${UNDERSTANDING}/timing-adjustable.html`,
|
|
190
|
+
classification: 'incomplete',
|
|
191
|
+
},
|
|
192
|
+
'time-limit-countdown': {
|
|
193
|
+
id: 'a11y-skills/time-limit-countdown',
|
|
194
|
+
sc: ['2.2.1'],
|
|
195
|
+
tags: ['a11y-skills', 'wcag2a', 'wcag221'],
|
|
196
|
+
impact: 'moderate',
|
|
197
|
+
scope: 'node',
|
|
198
|
+
description: 'Detect countdown/timeout wording in visible text',
|
|
199
|
+
help: 'Verify whether the countdown text indicates an adjustable time limit',
|
|
200
|
+
helpUrl: `${UNDERSTANDING}/timing-adjustable.html`,
|
|
201
|
+
classification: 'incomplete',
|
|
202
|
+
},
|
|
203
|
+
// --- auto-play-detection ---
|
|
204
|
+
'auto-play': {
|
|
205
|
+
id: 'a11y-skills/auto-play',
|
|
206
|
+
sc: ['2.2.2', '1.4.2'],
|
|
207
|
+
tags: ['a11y-skills', 'wcag2a', 'wcag222', 'wcag142'],
|
|
208
|
+
impact: 'moderate',
|
|
209
|
+
scope: 'page',
|
|
210
|
+
description: 'Detect auto-playing/moving content lasting longer than 5 seconds without a working pause control',
|
|
211
|
+
help: 'Moving content over 5 seconds needs a pause, stop, or hide mechanism',
|
|
212
|
+
helpUrl: `${UNDERSTANDING}/pause-stop-hide.html`,
|
|
213
|
+
// pixel diffing cannot identify the content type or audio.
|
|
214
|
+
classification: 'incomplete',
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
/** Look up a rule's metadata. */
|
|
218
|
+
export function getRule(key) {
|
|
219
|
+
return RULES[key];
|
|
220
|
+
}
|
|
@@ -34,11 +34,11 @@ export declare function resolveOutputPath(options: OutputLocationOptions & {
|
|
|
34
34
|
export interface SaveResultOptions extends OutputLocationOptions {
|
|
35
35
|
/** Default file name when none is provided via options. */
|
|
36
36
|
defaultFile: string;
|
|
37
|
-
/** Whether to append the disclaimer to the written JSON (default: true). */
|
|
38
|
-
includeDisclaimer?: boolean;
|
|
39
37
|
}
|
|
40
38
|
/**
|
|
41
39
|
* Save an audit result to a JSON file, creating parent directories as needed.
|
|
40
|
+
* The result is written as-is — the envelope built by `buildAuditResult()`
|
|
41
|
+
* already carries the disclaimer.
|
|
42
42
|
*
|
|
43
43
|
* @returns the absolute path the result was written to.
|
|
44
44
|
*/
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
-
import {
|
|
9
|
+
import { DISCLAIMER_CONSOLE } from '../constants.js';
|
|
10
10
|
/**
|
|
11
11
|
* Resolve the absolute path a check should write its result to.
|
|
12
12
|
*
|
|
@@ -33,17 +33,16 @@ export function resolveOutputPath(options) {
|
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
35
|
* Save an audit result to a JSON file, creating parent directories as needed.
|
|
36
|
+
* The result is written as-is — the envelope built by `buildAuditResult()`
|
|
37
|
+
* already carries the disclaimer.
|
|
36
38
|
*
|
|
37
39
|
* @returns the absolute path the result was written to.
|
|
38
40
|
*/
|
|
39
41
|
export function saveAuditResult(result, options) {
|
|
40
|
-
const { defaultFile,
|
|
42
|
+
const { defaultFile, ...location } = options;
|
|
41
43
|
const resolvedPath = resolveOutputPath({ ...location, defaultFile });
|
|
42
|
-
const outputData = includeDisclaimer
|
|
43
|
-
? { ...result, disclaimer: AUDIT_DISCLAIMER }
|
|
44
|
-
: result;
|
|
45
44
|
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
46
|
-
fs.writeFileSync(resolvedPath, JSON.stringify(
|
|
45
|
+
fs.writeFileSync(resolvedPath, JSON.stringify(result, null, 2));
|
|
47
46
|
return resolvedPath;
|
|
48
47
|
}
|
|
49
48
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a11y-skills/audit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Playwright + axe-core based WCAG 2.2 accessibility audit functions (axe, focus indicator, reflow, target size, text spacing, zoom, orientation, autocomplete, time limit, auto-play).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
"@playwright/test": "^1.50.0",
|
|
114
114
|
"@types/node": "^22.10.0",
|
|
115
115
|
"@types/pngjs": "^6.0.5",
|
|
116
|
+
"ajv": "^8.20.0",
|
|
116
117
|
"typescript": "^5.6.0"
|
|
117
118
|
},
|
|
118
119
|
"engines": {
|