@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.ja.md +76 -6
  3. package/README.md +78 -6
  4. package/dist/constants.d.ts +84 -0
  5. package/dist/constants.js +228 -0
  6. package/dist/detectors/index.d.ts +1 -0
  7. package/dist/detectors/index.js +1 -0
  8. package/dist/detectors/pause-control.d.ts +18 -0
  9. package/dist/detectors/pause-control.js +206 -0
  10. package/dist/index.d.ts +3 -1
  11. package/dist/index.js +3 -1
  12. package/dist/playwright/index.d.ts +7 -1
  13. package/dist/playwright/index.js +7 -1
  14. package/dist/playwright/runAutoPlayDetection.d.ts +36 -0
  15. package/dist/playwright/runAutoPlayDetection.js +143 -0
  16. package/dist/playwright/runAutocompleteAudit.d.ts +27 -0
  17. package/dist/playwright/runAutocompleteAudit.js +227 -0
  18. package/dist/playwright/runAxeAudit.d.ts +4 -0
  19. package/dist/playwright/runAxeAudit.js +26 -30
  20. package/dist/playwright/runFocusIndicatorCheck.js +55 -12
  21. package/dist/playwright/runOrientationCheck.d.ts +40 -0
  22. package/dist/playwright/runOrientationCheck.js +170 -0
  23. package/dist/playwright/runReflowCheck.js +18 -11
  24. package/dist/playwright/runTargetSizeCheck.js +42 -10
  25. package/dist/playwright/runTextSpacingCheck.d.ts +25 -0
  26. package/dist/playwright/runTextSpacingCheck.js +285 -0
  27. package/dist/playwright/runTimeLimitDetector.d.ts +31 -0
  28. package/dist/playwright/runTimeLimitDetector.js +219 -0
  29. package/dist/playwright/runZoomCheck.d.ts +42 -0
  30. package/dist/playwright/runZoomCheck.js +174 -0
  31. package/dist/schemas/index.d.ts +20 -1
  32. package/dist/schemas/index.js +404 -186
  33. package/dist/test-entries/auto-play-detection.d.ts +7 -0
  34. package/dist/test-entries/auto-play-detection.js +13 -0
  35. package/dist/test-entries/autocomplete-audit.d.ts +5 -0
  36. package/dist/test-entries/autocomplete-audit.js +11 -0
  37. package/dist/test-entries/orientation-check.d.ts +8 -0
  38. package/dist/test-entries/orientation-check.js +12 -0
  39. package/dist/test-entries/text-spacing-check.d.ts +5 -0
  40. package/dist/test-entries/text-spacing-check.js +11 -0
  41. package/dist/test-entries/time-limit-detector.d.ts +8 -0
  42. package/dist/test-entries/time-limit-detector.js +12 -0
  43. package/dist/test-entries/zoom-200-check.d.ts +5 -0
  44. package/dist/test-entries/zoom-200-check.js +11 -0
  45. package/dist/types.d.ts +275 -40
  46. package/dist/types.js +9 -0
  47. package/dist/utils/axe-format.d.ts +88 -0
  48. package/dist/utils/axe-format.js +361 -0
  49. package/dist/utils/image-compare.d.ts +24 -0
  50. package/dist/utils/image-compare.js +49 -0
  51. package/dist/utils/layout.d.ts +2 -0
  52. package/dist/utils/layout.js +20 -1
  53. package/dist/utils/recommendations.d.ts +18 -0
  54. package/dist/utils/recommendations.js +88 -0
  55. package/dist/utils/rule-registry.d.ts +216 -0
  56. package/dist/utils/rule-registry.js +220 -0
  57. package/dist/utils/test-harness.d.ts +8 -2
  58. package/dist/utils/test-harness.js +13 -6
  59. package/package.json +32 -2
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Compatibility test entry for auto-play detection (WCAG 1.4.2 / 2.2.2).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/auto-play-detection";`
4
+ *
5
+ * Requires the optional `pixelmatch` + `pngjs` deps.
6
+ */
7
+ import { test } from '@playwright/test';
8
+ import { runAutoPlayDetection } from '../playwright/runAutoPlayDetection.js';
9
+ import { requireTargetUrl } from '../utils/test-harness.js';
10
+ test('auto-play content detection', async ({ page }) => {
11
+ await page.goto(requireTargetUrl(), { waitUntil: 'networkidle' });
12
+ await runAutoPlayDetection({ page });
13
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Compatibility test entry for the autocomplete audit (WCAG 1.3.5).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/autocomplete-audit";`
4
+ */
5
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Compatibility test entry for the autocomplete audit (WCAG 1.3.5).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/autocomplete-audit";`
4
+ */
5
+ import { test } from '@playwright/test';
6
+ import { runAutocompleteAudit } from '../playwright/runAutocompleteAudit.js';
7
+ import { requireTargetUrl } from '../utils/test-harness.js';
8
+ test('autocomplete audit (WCAG 1.3.5)', async ({ page }) => {
9
+ await page.goto(requireTargetUrl(), { waitUntil: 'networkidle' });
10
+ await runAutocompleteAudit({ page });
11
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Compatibility test entry for the orientation check (WCAG 1.3.4).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/orientation-check";`
4
+ *
5
+ * This check owns navigation (it loads the page at two viewports), so the
6
+ * target URL comes from `TEST_PAGE` via the function.
7
+ */
8
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Compatibility test entry for the orientation check (WCAG 1.3.4).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/orientation-check";`
4
+ *
5
+ * This check owns navigation (it loads the page at two viewports), so the
6
+ * target URL comes from `TEST_PAGE` via the function.
7
+ */
8
+ import { test } from '@playwright/test';
9
+ import { runOrientationCheck } from '../playwright/runOrientationCheck.js';
10
+ test('orientation check (WCAG 1.3.4)', async ({ page }) => {
11
+ await runOrientationCheck({ page, screenshot: true });
12
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Compatibility test entry for the text spacing check (WCAG 1.4.12).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/text-spacing-check";`
4
+ */
5
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Compatibility test entry for the text spacing check (WCAG 1.4.12).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/text-spacing-check";`
4
+ */
5
+ import { test } from '@playwright/test';
6
+ import { runTextSpacingCheck } from '../playwright/runTextSpacingCheck.js';
7
+ import { requireTargetUrl } from '../utils/test-harness.js';
8
+ test('text spacing check (WCAG 1.4.12)', async ({ page }) => {
9
+ await page.goto(requireTargetUrl(), { waitUntil: 'networkidle' });
10
+ await runTextSpacingCheck({ page, screenshot: true });
11
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Compatibility test entry for the time limit detector (WCAG 2.2.1).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/time-limit-detector";`
4
+ *
5
+ * This check installs a timer hook before navigation, so it owns navigation;
6
+ * the target URL comes from `TEST_PAGE` via the function.
7
+ */
8
+ export {};
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Compatibility test entry for the time limit detector (WCAG 2.2.1).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/time-limit-detector";`
4
+ *
5
+ * This check installs a timer hook before navigation, so it owns navigation;
6
+ * the target URL comes from `TEST_PAGE` via the function.
7
+ */
8
+ import { test } from '@playwright/test';
9
+ import { runTimeLimitDetector } from '../playwright/runTimeLimitDetector.js';
10
+ test('time limit detector (WCAG 2.2.1)', async ({ page }) => {
11
+ await runTimeLimitDetector({ page });
12
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Compatibility test entry for the zoom 200% check (WCAG 1.4.4).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/zoom-200-check";`
4
+ */
5
+ export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Compatibility test entry for the zoom 200% check (WCAG 1.4.4).
3
+ * Run from a one-line local spec: `import "@a11y-skills/audit/test-entries/zoom-200-check";`
4
+ */
5
+ import { test } from '@playwright/test';
6
+ import { runZoomCheck } from '../playwright/runZoomCheck.js';
7
+ import { requireTargetUrl } from '../utils/test-harness.js';
8
+ test('zoom 200% check (WCAG 1.4.4)', async ({ page }) => {
9
+ // Let runZoomCheck navigate so the base viewport is applied before load.
10
+ await runZoomCheck({ page, targetUrl: requireTargetUrl(), screenshot: true });
11
+ });
package/dist/types.d.ts CHANGED
@@ -1,50 +1,117 @@
1
1
  /**
2
2
  * Type definitions for the WCAG audit checks shipped in @a11y-skills/audit.
3
+ *
4
+ * Every check returns the same axe-style envelope (`AuditCheckResult`):
5
+ * findings are normalized into `violations` / `incomplete` / `passes` /
6
+ * `inapplicable` rule arrays, while the check-specific evidence (measurements,
7
+ * screenshots, raw element records) lives under `details`.
8
+ *
9
+ * Classification policy: a finding is only a `violation` when the detection
10
+ * has no known blind spots and no WCAG exception could apply. Everything else
11
+ * (most heuristic detections) lands in `incomplete` — the manual-review queue.
3
12
  */
4
13
  import type { AUDIT_DISCLAIMER } from './constants.js';
5
- export interface AxeViolationNode {
6
- html: string;
14
+ /** Identifier of the check that produced a result. */
15
+ export type CheckSource = 'axe-audit' | 'focus-indicator-check' | 'reflow-check' | 'target-size-check' | 'text-spacing-check' | 'zoom-200-check' | 'orientation-check' | 'autocomplete-audit' | 'time-limit-detector' | 'auto-play-detection';
16
+ export type NormalizedImpact = 'critical' | 'serious' | 'moderate' | 'minor';
17
+ /** One affected element (or the page itself, `target: ['html']`). */
18
+ export interface NormalizedNode {
19
+ /** CSS selector path. Page-level findings use `['html']`. */
7
20
  target: string[];
8
- failureSummary: string | undefined;
21
+ /**
22
+ * outerHTML snippet (possibly truncated). When the source element's HTML
23
+ * could not be captured, a short synthetic representation is generated from
24
+ * the tag/role/name instead — never an empty string.
25
+ */
26
+ html: string;
27
+ /** Whether `html` was truncated to the snippet length limit. */
28
+ htmlTruncated: boolean;
29
+ /** Human-readable description of why this node was flagged. */
30
+ failureSummary: string;
9
31
  }
10
- export interface AxeViolation {
32
+ /** One rule's outcome, axe-style. */
33
+ export interface NormalizedRuleResult {
34
+ /** Namespaced rule id, e.g. `a11y-skills/focus-visible` (axe rules keep their own ids). */
11
35
  id: string;
12
- impact: string | null;
36
+ impact: NormalizedImpact | null;
13
37
  description: string;
14
38
  help: string;
39
+ /** W3C Understanding document (or axe docs for axe rules). */
15
40
  helpUrl: string;
41
+ /** axe-style tags: `a11y-skills`, `wcag2aa` / `wcag21aa` / `wcag22aa`, `wcag247`-style SC tags. */
16
42
  tags: string[];
17
- nodes: AxeViolationNode[];
43
+ nodes: NormalizedNode[];
44
+ }
45
+ /** Rule-level counts derived from the four buckets (not from `details`). */
46
+ export interface AuditResultSummary {
47
+ /** Number of rules in `violations`. */
48
+ violationCount: number;
49
+ /** Number of rules in `incomplete`. */
50
+ incompleteCount: number;
51
+ /** Number of rules in `passes`. */
52
+ passCount: number;
53
+ /** Number of elements the check examined, when the check can count them. */
54
+ checkedNodes?: number;
18
55
  }
19
- export interface AxeAuditResult {
56
+ /** Common envelope returned (and saved as JSON) by every check. */
57
+ export interface AuditCheckResult<TDetails> {
58
+ source: CheckSource;
20
59
  url: string;
21
60
  timestamp: string;
22
- violations: AxeViolation[];
23
- passes: number;
24
- incomplete: number;
25
- inapplicable: number;
26
- violationCount: number;
61
+ /** Confirmed findings — detection has no blind spot and no exception can apply. */
62
+ violations: NormalizedRuleResult[];
63
+ /** Findings that need manual confirmation (heuristic detections, possible exceptions). */
64
+ incomplete: NormalizedRuleResult[];
65
+ /** Rules that ran and found nothing (nodes omitted). */
66
+ passes: NormalizedRuleResult[];
67
+ /** Rules that had nothing to examine on this page. */
68
+ inapplicable: NormalizedRuleResult[];
69
+ summary: AuditResultSummary;
70
+ /** Check-specific evidence; sufficient to re-derive the buckets above. */
71
+ details: TDetails;
27
72
  disclaimer: typeof AUDIT_DISCLAIMER;
28
73
  }
74
+ /** Execution configuration; rule/node data is fully held in the envelope buckets. */
75
+ export interface AxeAuditDetails {
76
+ /** axe-core tags the run was filtered by. */
77
+ tagsRun: string[];
78
+ /** Rule overrides forwarded to axe, if any. */
79
+ rulesOverride: Record<string, {
80
+ enabled: boolean;
81
+ }> | null;
82
+ /** Raw axe result counts (rule-level). */
83
+ violationRuleCount: number;
84
+ passRuleCount: number;
85
+ incompleteRuleCount: number;
86
+ inapplicableRuleCount: number;
87
+ }
88
+ export type AxeAuditResult = AuditCheckResult<AxeAuditDetails>;
29
89
  export interface FocusRecord {
30
90
  id: number;
31
91
  tag: string;
32
92
  role: string | null;
33
93
  name: string;
94
+ selector: string;
95
+ html: string;
96
+ htmlTruncated: boolean;
34
97
  hasFocusStyle: boolean;
35
98
  diff: Record<string, string>;
36
99
  }
100
+ /** Reference to an element captured in the browser context. */
101
+ export interface FocusElementRef {
102
+ tag: string;
103
+ role: string | null;
104
+ name: string;
105
+ selector: string;
106
+ html: string;
107
+ htmlTruncated: boolean;
108
+ }
37
109
  /**
38
110
  * WCAG 3.2.1 On Focus violation - context change triggered by focus
39
111
  */
40
112
  export interface OnFocusViolation {
41
113
  /** Element that triggered the navigation */
42
- element: {
43
- tag: string;
44
- role: string | null;
45
- name: string;
46
- selector: string;
47
- };
114
+ element: FocusElementRef;
48
115
  /** URL before focus */
49
116
  fromUrl: string;
50
117
  /** URL after focus (navigation target) */
@@ -52,20 +119,15 @@ export interface OnFocusViolation {
52
119
  /** Type of context change */
53
120
  changeType: 'navigation' | 'new-window' | 'dialog';
54
121
  }
55
- export interface FocusCheckResult {
56
- url: string;
122
+ export interface FocusCheckDetails {
57
123
  totalFocusableElements: number;
58
124
  elementsWithFocusStyle: number;
59
125
  elementsWithoutFocusStyle: number;
60
- /** WCAG 2.4.7 violations */
61
- issues: Array<{
62
- tag: string;
63
- role: string | null;
64
- name: string;
65
- }>;
126
+ /** WCAG 2.4.7 findings (no computed-style change on focus) */
127
+ issues: FocusElementRef[];
66
128
  /** WCAG 3.2.1 violations - focus triggered context change */
67
129
  onFocusViolations: OnFocusViolation[];
68
- /** WCAG 2.4.12 violations - focus obscured by fixed/sticky elements */
130
+ /** WCAG 2.4.11/2.4.12 findings - focus obscured by fixed/sticky elements */
69
131
  focusObscuredIssues: FocusObscuredIssue[];
70
132
  elementsWithObscuredFocus: number;
71
133
  allElements: FocusRecord[];
@@ -78,6 +140,7 @@ export interface FocusCheckResult {
78
140
  */
79
141
  screenshotPath: string;
80
142
  }
143
+ export type FocusCheckResult = AuditCheckResult<FocusCheckDetails>;
81
144
  /**
82
145
  * Bounding rect for overlap calculations
83
146
  */
@@ -104,16 +167,11 @@ export interface FocusObscuredOverlap {
104
167
  overlapArea: number;
105
168
  }
106
169
  /**
107
- * WCAG 2.4.12 violation - focus indicator hidden by fixed/sticky content
170
+ * WCAG 2.4.11/2.4.12 finding - focus indicator hidden by fixed/sticky content
108
171
  */
109
172
  export interface FocusObscuredIssue {
110
173
  /** The focused element that is obscured */
111
- element: {
112
- tag: string;
113
- role: string | null;
114
- name: string;
115
- selector: string;
116
- };
174
+ element: FocusElementRef;
117
175
  /** Bounding rect of the focused element */
118
176
  elementRect: BoundingRect;
119
177
  /** List of overlapping fixed/sticky elements */
@@ -124,6 +182,8 @@ export interface FocusObscuredIssue {
124
182
  export interface ReflowIssue {
125
183
  selector: string;
126
184
  tagName: string;
185
+ html: string;
186
+ htmlTruncated: boolean;
127
187
  rect: {
128
188
  left: number;
129
189
  right: number;
@@ -135,6 +195,8 @@ export interface ReflowIssue {
135
195
  export interface ClippedTextElement {
136
196
  selector: string;
137
197
  tagName: string;
198
+ html: string;
199
+ htmlTruncated: boolean;
138
200
  scrollWidth: number;
139
201
  clientWidth: number;
140
202
  scrollHeight: number;
@@ -142,8 +204,7 @@ export interface ClippedTextElement {
142
204
  overflow: string;
143
205
  overflowX: string;
144
206
  }
145
- export interface ReflowCheckResult {
146
- url: string;
207
+ export interface ReflowCheckDetails {
147
208
  viewport: {
148
209
  width: number;
149
210
  height: number;
@@ -154,6 +215,7 @@ export interface ReflowCheckResult {
154
215
  overflowingElements: ReflowIssue[];
155
216
  clippedTextElements: ClippedTextElement[];
156
217
  }
218
+ export type ReflowCheckResult = AuditCheckResult<ReflowCheckDetails>;
157
219
  /**
158
220
  * Exception types for WCAG 2.5.8 Target Size (Minimum)
159
221
  * - inline: Target is in a sentence or text block
@@ -163,11 +225,22 @@ export interface ReflowCheckResult {
163
225
  * - essential-review: May be essential exception but requires manual review
164
226
  */
165
227
  export type TargetSizeException = 'inline' | 'redundant' | 'ua-control' | 'spacing' | 'essential-review';
228
+ /**
229
+ * How thoroughly the SC 2.5.8 exceptions were assessed for a target.
230
+ * - ruled-out: every exception was checked and none applies — the finding is a
231
+ * confirmed violation
232
+ * - possible: a heuristic matched an exception; needs manual confirmation
233
+ * - not-assessed: the heuristics found no exception, but they cannot rule out
234
+ * the essential exception — needs manual confirmation
235
+ */
236
+ export type TargetSizeExceptionAssessment = 'ruled-out' | 'possible' | 'not-assessed';
166
237
  export interface TargetSizeIssue {
167
238
  /** CSS selector for the element */
168
239
  selector: string;
169
240
  /** HTML tag name */
170
241
  tagName: string;
242
+ html: string;
243
+ htmlTruncated: boolean;
171
244
  /** ARIA role if present */
172
245
  role: string | null;
173
246
  /** Computed accessible name */
@@ -184,6 +257,8 @@ export interface TargetSizeIssue {
184
257
  exception: TargetSizeException | null;
185
258
  /** Human-readable exception details */
186
259
  exceptionDetails: string | null;
260
+ /** Exception-coverage of the assessment (drives violation vs incomplete) */
261
+ exceptionAssessment: TargetSizeExceptionAssessment;
187
262
  /** Link href for redundancy check */
188
263
  href: string | null;
189
264
  }
@@ -197,9 +272,7 @@ export interface TargetSizeSummary {
197
272
  /** Number of targets with possible exceptions */
198
273
  exceptedCount: number;
199
274
  }
200
- export interface TargetSizeCheckResult {
201
- /** Page URL */
202
- url: string;
275
+ export interface TargetSizeCheckDetails {
203
276
  /** Total interactive elements checked */
204
277
  totalTargetsChecked: number;
205
278
  /** Elements failing AA threshold (< 24px) */
@@ -210,6 +283,168 @@ export interface TargetSizeCheckResult {
210
283
  passedTargets: number;
211
284
  /** Elements with possible exceptions */
212
285
  exceptedTargets: TargetSizeIssue[];
213
- /** Summary counts */
286
+ /** Per-target counts */
214
287
  summary: TargetSizeSummary;
215
288
  }
289
+ export type TargetSizeCheckResult = AuditCheckResult<TargetSizeCheckDetails>;
290
+ export interface TextSpacingIssue {
291
+ selector: string;
292
+ tagName: string;
293
+ html: string;
294
+ htmlTruncated: boolean;
295
+ beforeMetrics: {
296
+ scrollWidth: number;
297
+ scrollHeight: number;
298
+ clientWidth: number;
299
+ clientHeight: number;
300
+ };
301
+ afterMetrics: {
302
+ scrollWidth: number;
303
+ scrollHeight: number;
304
+ clientWidth: number;
305
+ clientHeight: number;
306
+ };
307
+ overflow: string;
308
+ overflowX: string;
309
+ overflowY: string;
310
+ issueType: 'horizontal-clip' | 'vertical-clip' | 'both';
311
+ }
312
+ export interface TextSpacingCheckDetails {
313
+ clippedElements: TextSpacingIssue[];
314
+ totalElementsChecked: number;
315
+ }
316
+ export type TextSpacingCheckResult = AuditCheckResult<TextSpacingCheckDetails>;
317
+ export interface ZoomIssue {
318
+ selector: string;
319
+ tagName: string;
320
+ html: string;
321
+ htmlTruncated: boolean;
322
+ scrollWidth: number;
323
+ clientWidth: number;
324
+ scrollHeight: number;
325
+ clientHeight: number;
326
+ issueType: 'horizontal-scroll' | 'clipped-content';
327
+ }
328
+ export interface ZoomCheckDetails {
329
+ zoomFactor: number;
330
+ viewport: {
331
+ width: number;
332
+ height: number;
333
+ };
334
+ hasHorizontalScroll: boolean;
335
+ documentScrollWidth: number;
336
+ documentClientWidth: number;
337
+ clippedElements: ZoomIssue[];
338
+ }
339
+ export type ZoomCheckResult = AuditCheckResult<ZoomCheckDetails>;
340
+ export interface OrientationState {
341
+ lockMessageFound: boolean;
342
+ lockMessageText: string | null;
343
+ mainContentHidden: boolean;
344
+ bodyWidth: number;
345
+ bodyHeight: number;
346
+ visibleTextLength: number;
347
+ }
348
+ export interface OrientationCheckDetails {
349
+ portrait: OrientationState;
350
+ landscape: OrientationState;
351
+ hasOrientationLock: boolean;
352
+ lockDetectedIn: 'portrait' | 'landscape' | 'both' | 'none';
353
+ }
354
+ export type OrientationCheckResult = AuditCheckResult<OrientationCheckDetails>;
355
+ export interface AutocompleteIssue {
356
+ selector: string;
357
+ tagName: string;
358
+ html: string;
359
+ htmlTruncated: boolean;
360
+ inputType: string;
361
+ name: string | null;
362
+ id: string | null;
363
+ labelText: string | null;
364
+ currentAutocomplete: string | null;
365
+ expectedToken: string;
366
+ matchedBy: 'name' | 'id' | 'label' | 'placeholder';
367
+ issueType: 'missing' | 'invalid';
368
+ }
369
+ export interface AutocompleteAuditDetails {
370
+ totalFieldsChecked: number;
371
+ missingAutocomplete: AutocompleteIssue[];
372
+ invalidAutocomplete: AutocompleteIssue[];
373
+ }
374
+ export type AutocompleteAuditResult = AuditCheckResult<AutocompleteAuditDetails>;
375
+ export interface MetaRefreshInfo {
376
+ content: string;
377
+ seconds: number;
378
+ url: string | null;
379
+ html: string;
380
+ htmlTruncated: boolean;
381
+ }
382
+ export interface TimerInfo {
383
+ type: 'setTimeout' | 'setInterval';
384
+ delayMs: number;
385
+ callStack: string | null;
386
+ }
387
+ export interface CountdownIndicator {
388
+ selector: string;
389
+ text: string;
390
+ tagName: string;
391
+ html: string;
392
+ htmlTruncated: boolean;
393
+ }
394
+ export interface TimeLimitDetectorDetails {
395
+ metaRefresh: MetaRefreshInfo[];
396
+ timers: TimerInfo[];
397
+ countdownIndicators: CountdownIndicator[];
398
+ hasTimeLimits: boolean;
399
+ }
400
+ export type TimeLimitDetectorResult = AuditCheckResult<TimeLimitDetectorDetails>;
401
+ export interface ScreenshotRecord {
402
+ time: string;
403
+ path: string;
404
+ }
405
+ export interface ComparisonResult {
406
+ compare: string;
407
+ diffPixels: number;
408
+ totalPixels: number;
409
+ diffPercent: string;
410
+ hasChange: boolean;
411
+ }
412
+ export interface ImageDiffResult {
413
+ diffPixels: number;
414
+ totalPixels: number;
415
+ diffPercent: number;
416
+ }
417
+ export interface PauseControl {
418
+ element: string;
419
+ name: string;
420
+ matchedBy: 'accessible-name' | 'class-name-near-carousel' | 'svg-icon-pattern';
421
+ selector: string;
422
+ }
423
+ export interface CarouselIndicator {
424
+ element: string;
425
+ name: string;
426
+ }
427
+ export interface PauseControlInfo {
428
+ found: boolean;
429
+ controls: PauseControl[];
430
+ carouselIndicators: CarouselIndicator[];
431
+ hasAccessibleName: boolean;
432
+ }
433
+ export interface PauseVerificationResult {
434
+ attempted: boolean;
435
+ controlClicked: string | null;
436
+ beforeClickDiffPercent: string | null;
437
+ afterClickDiffPercent: string | null;
438
+ pauseWorked: boolean | null;
439
+ error: string | null;
440
+ }
441
+ export interface AutoPlayDetectionDetails {
442
+ screenshotRecords: ScreenshotRecord[];
443
+ comparisons: ComparisonResult[];
444
+ hasAutoPlayContent: boolean;
445
+ stopsWithin5Seconds: boolean;
446
+ pauseControls: PauseControlInfo;
447
+ pauseVerification: PauseVerificationResult;
448
+ recommendation: string;
449
+ }
450
+ export type AutoPlayDetectionResult = AuditCheckResult<AutoPlayDetectionDetails>;
package/dist/types.js CHANGED
@@ -1,4 +1,13 @@
1
1
  /**
2
2
  * Type definitions for the WCAG audit checks shipped in @a11y-skills/audit.
3
+ *
4
+ * Every check returns the same axe-style envelope (`AuditCheckResult`):
5
+ * findings are normalized into `violations` / `incomplete` / `passes` /
6
+ * `inapplicable` rule arrays, while the check-specific evidence (measurements,
7
+ * screenshots, raw element records) lives under `details`.
8
+ *
9
+ * Classification policy: a finding is only a `violation` when the detection
10
+ * has no known blind spots and no WCAG exception could apply. Everything else
11
+ * (most heuristic detections) lands in `incomplete` — the manual-review queue.
3
12
  */
4
13
  export {};
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Pure mappers from check-specific `details` to the axe-style buckets
3
+ * (`violations` / `incomplete` / `passes` / `inapplicable`), plus the envelope
4
+ * assembler and the opt-in cross-check merge.
5
+ *
6
+ * Everything here is browser-independent: the runners gather evidence into a
7
+ * details object, and these functions derive the normalized view from it.
8
+ * Because the details objects are part of the saved JSON, the buckets can be
9
+ * re-derived from a result file at any time.
10
+ */
11
+ import type { AuditCheckResult, AuditResultSummary, AutocompleteAuditDetails, AutoPlayDetectionDetails, CheckSource, FocusCheckDetails, NormalizedRuleResult, OrientationCheckDetails, ReflowCheckDetails, TargetSizeCheckDetails, TextSpacingCheckDetails, TimeLimitDetectorDetails, ZoomCheckDetails } from '../types.js';
12
+ import { AUDIT_DISCLAIMER } from '../constants.js';
13
+ export interface NormalizedBuckets {
14
+ violations: NormalizedRuleResult[];
15
+ incomplete: NormalizedRuleResult[];
16
+ passes: NormalizedRuleResult[];
17
+ inapplicable: NormalizedRuleResult[];
18
+ /** Number of elements the check examined, when countable. */
19
+ checkedNodes?: number;
20
+ }
21
+ /** Assemble the common envelope from a check's details and buckets. */
22
+ export declare function buildAuditResult<TDetails>(args: {
23
+ source: CheckSource;
24
+ url: string;
25
+ details: TDetails;
26
+ buckets: NormalizedBuckets;
27
+ timestamp?: string;
28
+ }): AuditCheckResult<TDetails>;
29
+ export declare function normalizeFocusCheck(details: FocusCheckDetails): NormalizedBuckets;
30
+ export declare function normalizeReflowCheck(details: ReflowCheckDetails): NormalizedBuckets;
31
+ export declare function normalizeTargetSizeCheck(details: TargetSizeCheckDetails): NormalizedBuckets;
32
+ export declare function normalizeTextSpacingCheck(details: TextSpacingCheckDetails): NormalizedBuckets;
33
+ export declare function normalizeZoomCheck(details: ZoomCheckDetails): NormalizedBuckets;
34
+ export declare function normalizeOrientationCheck(details: OrientationCheckDetails): NormalizedBuckets;
35
+ export declare function normalizeAutocompleteAudit(details: AutocompleteAuditDetails): NormalizedBuckets;
36
+ export declare function normalizeTimeLimitDetector(details: TimeLimitDetectorDetails): NormalizedBuckets;
37
+ export declare function normalizeAutoPlayDetection(details: AutoPlayDetectionDetails): NormalizedBuckets;
38
+ /** Structural subset of an axe-core rule result (avoids a hard axe dependency). */
39
+ export interface RawAxeRule {
40
+ id: string;
41
+ impact?: string | null;
42
+ description: string;
43
+ help: string;
44
+ helpUrl: string;
45
+ tags: string[];
46
+ nodes: Array<{
47
+ html: string;
48
+ target: unknown[];
49
+ failureSummary?: string | undefined;
50
+ }>;
51
+ }
52
+ export interface RawAxeResults {
53
+ violations: RawAxeRule[];
54
+ incomplete: RawAxeRule[];
55
+ passes: RawAxeRule[];
56
+ inapplicable: RawAxeRule[];
57
+ }
58
+ /**
59
+ * Normalize raw axe-core results into the common buckets. Must be fed the RAW
60
+ * `AxeResults` (before any reduction) — pass/incomplete details are not
61
+ * recoverable afterwards. Node lists are kept for violations and incomplete;
62
+ * passes/inapplicable carry rule metadata only.
63
+ */
64
+ export declare function normalizeAxeResults(raw: RawAxeResults): NormalizedBuckets;
65
+ /** Combined view over several checks of the SAME page. */
66
+ export interface MergedAuditResult {
67
+ url: string;
68
+ /** Latest timestamp among the merged results. */
69
+ timestamp: string;
70
+ sources: CheckSource[];
71
+ violations: NormalizedRuleResult[];
72
+ incomplete: NormalizedRuleResult[];
73
+ passes: NormalizedRuleResult[];
74
+ inapplicable: NormalizedRuleResult[];
75
+ summary: AuditResultSummary;
76
+ disclaimer: typeof AUDIT_DISCLAIMER;
77
+ }
78
+ /**
79
+ * Merge several check results for the same URL into one normalized view.
80
+ *
81
+ * - Results with differing URLs are rejected (throws).
82
+ * - The same rule id is merged into one entry; nodes are deduplicated by
83
+ * `target` + `failureSummary`. Identical selectors inside different frames
84
+ * or shadow roots are NOT distinguished.
85
+ * - A rule appearing in several buckets is placed in the highest-priority one:
86
+ * violations > incomplete > passes > inapplicable.
87
+ */
88
+ export declare function mergeNormalizedResults(results: Array<AuditCheckResult<unknown>>): MergedAuditResult;