@a11y-skills/audit 0.1.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 +65 -0
- package/README.ja.md +76 -6
- package/README.md +78 -6
- package/dist/constants.d.ts +84 -0
- package/dist/constants.js +228 -0
- package/dist/detectors/index.d.ts +1 -0
- package/dist/detectors/index.js +1 -0
- package/dist/detectors/pause-control.d.ts +18 -0
- package/dist/detectors/pause-control.js +206 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/playwright/index.d.ts +7 -1
- package/dist/playwright/index.js +7 -1
- package/dist/playwright/runAutoPlayDetection.d.ts +36 -0
- package/dist/playwright/runAutoPlayDetection.js +143 -0
- package/dist/playwright/runAutocompleteAudit.d.ts +27 -0
- package/dist/playwright/runAutocompleteAudit.js +227 -0
- 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.d.ts +40 -0
- package/dist/playwright/runOrientationCheck.js +170 -0
- package/dist/playwright/runReflowCheck.js +18 -11
- package/dist/playwright/runTargetSizeCheck.js +42 -10
- package/dist/playwright/runTextSpacingCheck.d.ts +25 -0
- package/dist/playwright/runTextSpacingCheck.js +285 -0
- package/dist/playwright/runTimeLimitDetector.d.ts +31 -0
- package/dist/playwright/runTimeLimitDetector.js +219 -0
- package/dist/playwright/runZoomCheck.d.ts +42 -0
- package/dist/playwright/runZoomCheck.js +174 -0
- package/dist/schemas/index.d.ts +20 -1
- package/dist/schemas/index.js +404 -186
- package/dist/test-entries/auto-play-detection.d.ts +7 -0
- package/dist/test-entries/auto-play-detection.js +13 -0
- package/dist/test-entries/autocomplete-audit.d.ts +5 -0
- package/dist/test-entries/autocomplete-audit.js +11 -0
- package/dist/test-entries/orientation-check.d.ts +8 -0
- package/dist/test-entries/orientation-check.js +12 -0
- package/dist/test-entries/text-spacing-check.d.ts +5 -0
- package/dist/test-entries/text-spacing-check.js +11 -0
- package/dist/test-entries/time-limit-detector.d.ts +8 -0
- package/dist/test-entries/time-limit-detector.js +12 -0
- package/dist/test-entries/zoom-200-check.d.ts +5 -0
- package/dist/test-entries/zoom-200-check.js +11 -0
- package/dist/types.d.ts +275 -40
- 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/image-compare.d.ts +24 -0
- package/dist/utils/image-compare.js +49 -0
- package/dist/utils/layout.d.ts +2 -0
- package/dist/utils/layout.js +20 -1
- package/dist/utils/recommendations.d.ts +18 -0
- package/dist/utils/recommendations.js +88 -0
- package/dist/utils/rule-registry.d.ts +216 -0
- package/dist/utils/rule-registry.js +220 -0
- package/dist/utils/test-harness.d.ts +8 -2
- package/dist/utils/test-harness.js +13 -6
- package/package.json +32 -2
|
@@ -0,0 +1,216 @@
|
|
|
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
|
+
import type { NormalizedImpact } from '../types.js';
|
|
16
|
+
export interface RuleMeta {
|
|
17
|
+
/** Namespaced rule id, e.g. `a11y-skills/focus-visible`. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** WCAG success criteria, e.g. `['2.4.7']`. */
|
|
20
|
+
sc: string[];
|
|
21
|
+
/**
|
|
22
|
+
* axe-style tags. The version+level tag reflects the WCAG version that
|
|
23
|
+
* introduced the SC (2.4.7 → `wcag2aa`, 1.4.10 → `wcag21aa`, ...).
|
|
24
|
+
*/
|
|
25
|
+
tags: string[];
|
|
26
|
+
impact: NormalizedImpact;
|
|
27
|
+
/** Whether findings refer to specific elements or the page as a whole. */
|
|
28
|
+
scope: 'node' | 'page';
|
|
29
|
+
description: string;
|
|
30
|
+
help: string;
|
|
31
|
+
/** W3C Understanding document URL. */
|
|
32
|
+
helpUrl: string;
|
|
33
|
+
/** Default bucket for findings of this rule. */
|
|
34
|
+
classification: 'violation' | 'incomplete';
|
|
35
|
+
}
|
|
36
|
+
export declare const RULES: {
|
|
37
|
+
readonly 'focus-visible': {
|
|
38
|
+
readonly id: "a11y-skills/focus-visible";
|
|
39
|
+
readonly sc: ["2.4.7"];
|
|
40
|
+
readonly tags: ["a11y-skills", "wcag2aa", "wcag247"];
|
|
41
|
+
readonly impact: "serious";
|
|
42
|
+
readonly scope: "node";
|
|
43
|
+
readonly description: "Ensure keyboard focus produces a visible indicator on focusable elements";
|
|
44
|
+
readonly help: "Focusable elements should have a visible focus indicator";
|
|
45
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html";
|
|
46
|
+
readonly classification: "incomplete";
|
|
47
|
+
};
|
|
48
|
+
readonly 'no-context-change-on-focus': {
|
|
49
|
+
readonly id: "a11y-skills/no-context-change-on-focus";
|
|
50
|
+
readonly sc: ["3.2.1"];
|
|
51
|
+
readonly tags: ["a11y-skills", "wcag2a", "wcag321"];
|
|
52
|
+
readonly impact: "serious";
|
|
53
|
+
readonly scope: "node";
|
|
54
|
+
readonly description: "Ensure receiving focus does not trigger a change of context (navigation, new window)";
|
|
55
|
+
readonly help: "Focusing an element must not trigger a context change";
|
|
56
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/on-focus.html";
|
|
57
|
+
readonly classification: "violation";
|
|
58
|
+
};
|
|
59
|
+
readonly 'focus-not-obscured': {
|
|
60
|
+
readonly id: "a11y-skills/focus-not-obscured";
|
|
61
|
+
readonly sc: ["2.4.11", "2.4.12"];
|
|
62
|
+
readonly tags: ["a11y-skills", "wcag22aa", "wcag2411"];
|
|
63
|
+
readonly impact: "moderate";
|
|
64
|
+
readonly scope: "node";
|
|
65
|
+
readonly description: "Ensure the focused element is not hidden by fixed or sticky content";
|
|
66
|
+
readonly help: "Focused elements should not be obscured by author-created content";
|
|
67
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html";
|
|
68
|
+
readonly classification: "incomplete";
|
|
69
|
+
};
|
|
70
|
+
readonly 'reflow-overflow': {
|
|
71
|
+
readonly id: "a11y-skills/reflow-overflow";
|
|
72
|
+
readonly sc: ["1.4.10"];
|
|
73
|
+
readonly tags: ["a11y-skills", "wcag21aa", "wcag1410"];
|
|
74
|
+
readonly impact: "serious";
|
|
75
|
+
readonly scope: "node";
|
|
76
|
+
readonly description: "Ensure content reflows at 320 CSS px width without horizontal scrolling";
|
|
77
|
+
readonly help: "Content should not require horizontal scrolling at 320px width";
|
|
78
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/reflow.html";
|
|
79
|
+
readonly classification: "incomplete";
|
|
80
|
+
};
|
|
81
|
+
readonly 'reflow-clipped-text': {
|
|
82
|
+
readonly id: "a11y-skills/reflow-clipped-text";
|
|
83
|
+
readonly sc: ["1.4.10"];
|
|
84
|
+
readonly tags: ["a11y-skills", "wcag21aa", "wcag1410"];
|
|
85
|
+
readonly impact: "moderate";
|
|
86
|
+
readonly scope: "node";
|
|
87
|
+
readonly description: "Ensure text is not clipped when content reflows at 320 CSS px";
|
|
88
|
+
readonly help: "Text should remain readable at 320px width";
|
|
89
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/reflow.html";
|
|
90
|
+
readonly classification: "incomplete";
|
|
91
|
+
};
|
|
92
|
+
readonly 'target-size-minimum': {
|
|
93
|
+
readonly id: "a11y-skills/target-size-minimum";
|
|
94
|
+
readonly sc: ["2.5.8"];
|
|
95
|
+
readonly tags: ["a11y-skills", "wcag22aa", "wcag258"];
|
|
96
|
+
readonly impact: "serious";
|
|
97
|
+
readonly scope: "node";
|
|
98
|
+
readonly description: "Ensure pointer targets are at least 24x24 CSS px (WCAG 2.5.8 AA)";
|
|
99
|
+
readonly help: "Pointer targets should be at least 24x24 CSS px";
|
|
100
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html";
|
|
101
|
+
readonly classification: "incomplete";
|
|
102
|
+
};
|
|
103
|
+
readonly 'target-size-enhanced': {
|
|
104
|
+
readonly id: "a11y-skills/target-size-enhanced";
|
|
105
|
+
readonly sc: ["2.5.5"];
|
|
106
|
+
readonly tags: ["a11y-skills", "wcag21aaa", "wcag255"];
|
|
107
|
+
readonly impact: "moderate";
|
|
108
|
+
readonly scope: "node";
|
|
109
|
+
readonly description: "Ensure pointer targets are at least 44x44 CSS px (WCAG 2.5.5 AAA)";
|
|
110
|
+
readonly help: "Pointer targets should be at least 44x44 CSS px";
|
|
111
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html";
|
|
112
|
+
readonly classification: "incomplete";
|
|
113
|
+
};
|
|
114
|
+
readonly 'text-spacing': {
|
|
115
|
+
readonly id: "a11y-skills/text-spacing";
|
|
116
|
+
readonly sc: ["1.4.12"];
|
|
117
|
+
readonly tags: ["a11y-skills", "wcag21aa", "wcag1412"];
|
|
118
|
+
readonly impact: "moderate";
|
|
119
|
+
readonly scope: "node";
|
|
120
|
+
readonly description: "Ensure no loss of content when WCAG 1.4.12 text spacing overrides are applied";
|
|
121
|
+
readonly help: "Text must not be clipped under increased text spacing";
|
|
122
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/text-spacing.html";
|
|
123
|
+
readonly classification: "violation";
|
|
124
|
+
};
|
|
125
|
+
readonly 'resize-text': {
|
|
126
|
+
readonly id: "a11y-skills/resize-text";
|
|
127
|
+
readonly sc: ["1.4.4"];
|
|
128
|
+
readonly tags: ["a11y-skills", "wcag2aa", "wcag144"];
|
|
129
|
+
readonly impact: "moderate";
|
|
130
|
+
readonly scope: "node";
|
|
131
|
+
readonly description: "Ensure content remains usable when text is resized to 200%";
|
|
132
|
+
readonly help: "Content should not be lost or clipped at 200% zoom";
|
|
133
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html";
|
|
134
|
+
readonly classification: "incomplete";
|
|
135
|
+
};
|
|
136
|
+
readonly 'orientation-lock': {
|
|
137
|
+
readonly id: "a11y-skills/orientation-lock";
|
|
138
|
+
readonly sc: ["1.3.4"];
|
|
139
|
+
readonly tags: ["a11y-skills", "wcag21aa", "wcag134"];
|
|
140
|
+
readonly impact: "serious";
|
|
141
|
+
readonly scope: "page";
|
|
142
|
+
readonly description: "Ensure content does not restrict its view to a single display orientation";
|
|
143
|
+
readonly help: "Content should work in both portrait and landscape orientation";
|
|
144
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/orientation.html";
|
|
145
|
+
readonly classification: "incomplete";
|
|
146
|
+
};
|
|
147
|
+
readonly 'autocomplete-invalid': {
|
|
148
|
+
readonly id: "a11y-skills/autocomplete-invalid";
|
|
149
|
+
readonly sc: ["1.3.5"];
|
|
150
|
+
readonly tags: ["a11y-skills", "wcag21aa", "wcag135"];
|
|
151
|
+
readonly impact: "moderate";
|
|
152
|
+
readonly scope: "node";
|
|
153
|
+
readonly description: "Ensure autocomplete attribute values are valid tokens";
|
|
154
|
+
readonly help: "autocomplete attributes must use valid token values";
|
|
155
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html";
|
|
156
|
+
readonly classification: "violation";
|
|
157
|
+
};
|
|
158
|
+
readonly 'autocomplete-missing': {
|
|
159
|
+
readonly id: "a11y-skills/autocomplete-missing";
|
|
160
|
+
readonly sc: ["1.3.5"];
|
|
161
|
+
readonly tags: ["a11y-skills", "wcag21aa", "wcag135"];
|
|
162
|
+
readonly impact: "moderate";
|
|
163
|
+
readonly scope: "node";
|
|
164
|
+
readonly description: "Ensure fields collecting user information declare their purpose via autocomplete";
|
|
165
|
+
readonly help: "Add an autocomplete attribute matching the field purpose";
|
|
166
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/identify-input-purpose.html";
|
|
167
|
+
readonly classification: "incomplete";
|
|
168
|
+
};
|
|
169
|
+
readonly 'meta-refresh': {
|
|
170
|
+
readonly id: "a11y-skills/meta-refresh";
|
|
171
|
+
readonly sc: ["2.2.1"];
|
|
172
|
+
readonly tags: ["a11y-skills", "wcag2a", "wcag221"];
|
|
173
|
+
readonly impact: "serious";
|
|
174
|
+
readonly scope: "node";
|
|
175
|
+
readonly description: "Ensure meta refresh time limits can be turned off, adjusted, or extended";
|
|
176
|
+
readonly help: "Verify the meta refresh satisfies an SC 2.2.1 exception or is adjustable";
|
|
177
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html";
|
|
178
|
+
readonly classification: "incomplete";
|
|
179
|
+
};
|
|
180
|
+
readonly 'time-limit-timer': {
|
|
181
|
+
readonly id: "a11y-skills/time-limit-timer";
|
|
182
|
+
readonly sc: ["2.2.1"];
|
|
183
|
+
readonly tags: ["a11y-skills", "wcag2a", "wcag221"];
|
|
184
|
+
readonly impact: "moderate";
|
|
185
|
+
readonly scope: "page";
|
|
186
|
+
readonly description: "Detect JavaScript timers that may implement a time limit";
|
|
187
|
+
readonly help: "Verify whether detected timers implement an adjustable time limit";
|
|
188
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html";
|
|
189
|
+
readonly classification: "incomplete";
|
|
190
|
+
};
|
|
191
|
+
readonly 'time-limit-countdown': {
|
|
192
|
+
readonly id: "a11y-skills/time-limit-countdown";
|
|
193
|
+
readonly sc: ["2.2.1"];
|
|
194
|
+
readonly tags: ["a11y-skills", "wcag2a", "wcag221"];
|
|
195
|
+
readonly impact: "moderate";
|
|
196
|
+
readonly scope: "node";
|
|
197
|
+
readonly description: "Detect countdown/timeout wording in visible text";
|
|
198
|
+
readonly help: "Verify whether the countdown text indicates an adjustable time limit";
|
|
199
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html";
|
|
200
|
+
readonly classification: "incomplete";
|
|
201
|
+
};
|
|
202
|
+
readonly 'auto-play': {
|
|
203
|
+
readonly id: "a11y-skills/auto-play";
|
|
204
|
+
readonly sc: ["2.2.2", "1.4.2"];
|
|
205
|
+
readonly tags: ["a11y-skills", "wcag2a", "wcag222", "wcag142"];
|
|
206
|
+
readonly impact: "moderate";
|
|
207
|
+
readonly scope: "page";
|
|
208
|
+
readonly description: "Detect auto-playing/moving content lasting longer than 5 seconds without a working pause control";
|
|
209
|
+
readonly help: "Moving content over 5 seconds needs a pause, stop, or hide mechanism";
|
|
210
|
+
readonly helpUrl: "https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html";
|
|
211
|
+
readonly classification: "incomplete";
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
export type RuleKey = keyof typeof RULES;
|
|
215
|
+
/** Look up a rule's metadata. */
|
|
216
|
+
export declare function getRule(key: RuleKey): RuleMeta;
|
|
@@ -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
|
*/
|
|
@@ -66,6 +66,12 @@ export declare function resolveScreenshotPath(resolvedResultPath: string, defaul
|
|
|
66
66
|
* @throws if neither is provided.
|
|
67
67
|
*/
|
|
68
68
|
export declare function requireTargetUrl(explicit?: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the target URL from the `TEST_PAGE` env var, falling back to the
|
|
71
|
+
* given default. Useful for callers that want a sensible default (e.g. a test
|
|
72
|
+
* fixture URL or preset) instead of throwing when `TEST_PAGE` is unset.
|
|
73
|
+
*/
|
|
74
|
+
export declare function getTargetUrl(defaultPath: string): string;
|
|
69
75
|
/** Log the header for audit results to console. */
|
|
70
76
|
export declare function logAuditHeader(title: string, wcagRef: string, url: string): void;
|
|
71
77
|
/** Log a summary section with key-value pairs. */
|
|
@@ -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
|
/**
|
|
@@ -80,6 +79,14 @@ export function requireTargetUrl(explicit) {
|
|
|
80
79
|
}
|
|
81
80
|
return url;
|
|
82
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the target URL from the `TEST_PAGE` env var, falling back to the
|
|
84
|
+
* given default. Useful for callers that want a sensible default (e.g. a test
|
|
85
|
+
* fixture URL or preset) instead of throwing when `TEST_PAGE` is unset.
|
|
86
|
+
*/
|
|
87
|
+
export function getTargetUrl(defaultPath) {
|
|
88
|
+
return process.env.TEST_PAGE || defaultPath;
|
|
89
|
+
}
|
|
83
90
|
// =============================================================================
|
|
84
91
|
// Console Logging
|
|
85
92
|
// =============================================================================
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a11y-skills/audit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Playwright + axe-core based WCAG 2.2 accessibility audit functions (axe, focus indicator, reflow, target size).",
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"author": {
|
|
@@ -57,6 +57,30 @@
|
|
|
57
57
|
"./test-entries/target-size-check": {
|
|
58
58
|
"types": "./dist/test-entries/target-size-check.d.ts",
|
|
59
59
|
"import": "./dist/test-entries/target-size-check.js"
|
|
60
|
+
},
|
|
61
|
+
"./test-entries/text-spacing-check": {
|
|
62
|
+
"types": "./dist/test-entries/text-spacing-check.d.ts",
|
|
63
|
+
"import": "./dist/test-entries/text-spacing-check.js"
|
|
64
|
+
},
|
|
65
|
+
"./test-entries/zoom-200-check": {
|
|
66
|
+
"types": "./dist/test-entries/zoom-200-check.d.ts",
|
|
67
|
+
"import": "./dist/test-entries/zoom-200-check.js"
|
|
68
|
+
},
|
|
69
|
+
"./test-entries/orientation-check": {
|
|
70
|
+
"types": "./dist/test-entries/orientation-check.d.ts",
|
|
71
|
+
"import": "./dist/test-entries/orientation-check.js"
|
|
72
|
+
},
|
|
73
|
+
"./test-entries/autocomplete-audit": {
|
|
74
|
+
"types": "./dist/test-entries/autocomplete-audit.d.ts",
|
|
75
|
+
"import": "./dist/test-entries/autocomplete-audit.js"
|
|
76
|
+
},
|
|
77
|
+
"./test-entries/time-limit-detector": {
|
|
78
|
+
"types": "./dist/test-entries/time-limit-detector.d.ts",
|
|
79
|
+
"import": "./dist/test-entries/time-limit-detector.js"
|
|
80
|
+
},
|
|
81
|
+
"./test-entries/auto-play-detection": {
|
|
82
|
+
"types": "./dist/test-entries/auto-play-detection.d.ts",
|
|
83
|
+
"import": "./dist/test-entries/auto-play-detection.js"
|
|
60
84
|
}
|
|
61
85
|
},
|
|
62
86
|
"files": [
|
|
@@ -80,10 +104,16 @@
|
|
|
80
104
|
"@axe-core/playwright": "^4.10.0",
|
|
81
105
|
"@playwright/test": "^1.50.0"
|
|
82
106
|
},
|
|
107
|
+
"optionalDependencies": {
|
|
108
|
+
"pixelmatch": "^7.1.0",
|
|
109
|
+
"pngjs": "^7.0.0"
|
|
110
|
+
},
|
|
83
111
|
"devDependencies": {
|
|
84
112
|
"@axe-core/playwright": "^4.10.0",
|
|
85
113
|
"@playwright/test": "^1.50.0",
|
|
86
114
|
"@types/node": "^22.10.0",
|
|
115
|
+
"@types/pngjs": "^6.0.5",
|
|
116
|
+
"ajv": "^8.20.0",
|
|
87
117
|
"typescript": "^5.6.0"
|
|
88
118
|
},
|
|
89
119
|
"engines": {
|