@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/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;
9
- }
10
- export interface AxeViolation {
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;
31
+ }
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[];
18
44
  }
19
- export interface AxeAuditResult {
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;
55
+ }
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,12 +283,15 @@ 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>;
216
290
  export interface TextSpacingIssue {
217
291
  selector: string;
218
292
  tagName: string;
293
+ html: string;
294
+ htmlTruncated: boolean;
219
295
  beforeMetrics: {
220
296
  scrollWidth: number;
221
297
  scrollHeight: number;
@@ -233,22 +309,23 @@ export interface TextSpacingIssue {
233
309
  overflowY: string;
234
310
  issueType: 'horizontal-clip' | 'vertical-clip' | 'both';
235
311
  }
236
- export interface TextSpacingCheckResult {
237
- url: string;
312
+ export interface TextSpacingCheckDetails {
238
313
  clippedElements: TextSpacingIssue[];
239
314
  totalElementsChecked: number;
240
315
  }
316
+ export type TextSpacingCheckResult = AuditCheckResult<TextSpacingCheckDetails>;
241
317
  export interface ZoomIssue {
242
318
  selector: string;
243
319
  tagName: string;
320
+ html: string;
321
+ htmlTruncated: boolean;
244
322
  scrollWidth: number;
245
323
  clientWidth: number;
246
324
  scrollHeight: number;
247
325
  clientHeight: number;
248
326
  issueType: 'horizontal-scroll' | 'clipped-content';
249
327
  }
250
- export interface ZoomCheckResult {
251
- url: string;
328
+ export interface ZoomCheckDetails {
252
329
  zoomFactor: number;
253
330
  viewport: {
254
331
  width: number;
@@ -259,6 +336,7 @@ export interface ZoomCheckResult {
259
336
  documentClientWidth: number;
260
337
  clippedElements: ZoomIssue[];
261
338
  }
339
+ export type ZoomCheckResult = AuditCheckResult<ZoomCheckDetails>;
262
340
  export interface OrientationState {
263
341
  lockMessageFound: boolean;
264
342
  lockMessageText: string | null;
@@ -267,16 +345,18 @@ export interface OrientationState {
267
345
  bodyHeight: number;
268
346
  visibleTextLength: number;
269
347
  }
270
- export interface OrientationCheckResult {
271
- url: string;
348
+ export interface OrientationCheckDetails {
272
349
  portrait: OrientationState;
273
350
  landscape: OrientationState;
274
351
  hasOrientationLock: boolean;
275
352
  lockDetectedIn: 'portrait' | 'landscape' | 'both' | 'none';
276
353
  }
354
+ export type OrientationCheckResult = AuditCheckResult<OrientationCheckDetails>;
277
355
  export interface AutocompleteIssue {
278
356
  selector: string;
279
357
  tagName: string;
358
+ html: string;
359
+ htmlTruncated: boolean;
280
360
  inputType: string;
281
361
  name: string | null;
282
362
  id: string | null;
@@ -286,16 +366,18 @@ export interface AutocompleteIssue {
286
366
  matchedBy: 'name' | 'id' | 'label' | 'placeholder';
287
367
  issueType: 'missing' | 'invalid';
288
368
  }
289
- export interface AutocompleteAuditResult {
290
- url: string;
369
+ export interface AutocompleteAuditDetails {
291
370
  totalFieldsChecked: number;
292
371
  missingAutocomplete: AutocompleteIssue[];
293
372
  invalidAutocomplete: AutocompleteIssue[];
294
373
  }
374
+ export type AutocompleteAuditResult = AuditCheckResult<AutocompleteAuditDetails>;
295
375
  export interface MetaRefreshInfo {
296
376
  content: string;
297
377
  seconds: number;
298
378
  url: string | null;
379
+ html: string;
380
+ htmlTruncated: boolean;
299
381
  }
300
382
  export interface TimerInfo {
301
383
  type: 'setTimeout' | 'setInterval';
@@ -306,14 +388,16 @@ export interface CountdownIndicator {
306
388
  selector: string;
307
389
  text: string;
308
390
  tagName: string;
391
+ html: string;
392
+ htmlTruncated: boolean;
309
393
  }
310
- export interface TimeLimitDetectorResult {
311
- url: string;
394
+ export interface TimeLimitDetectorDetails {
312
395
  metaRefresh: MetaRefreshInfo[];
313
396
  timers: TimerInfo[];
314
397
  countdownIndicators: CountdownIndicator[];
315
398
  hasTimeLimits: boolean;
316
399
  }
400
+ export type TimeLimitDetectorResult = AuditCheckResult<TimeLimitDetectorDetails>;
317
401
  export interface ScreenshotRecord {
318
402
  time: string;
319
403
  path: string;
@@ -354,8 +438,7 @@ export interface PauseVerificationResult {
354
438
  pauseWorked: boolean | null;
355
439
  error: string | null;
356
440
  }
357
- export interface AutoPlayDetectionResult {
358
- url: string;
441
+ export interface AutoPlayDetectionDetails {
359
442
  screenshotRecords: ScreenshotRecord[];
360
443
  comparisons: ComparisonResult[];
361
444
  hasAutoPlayContent: boolean;
@@ -364,3 +447,4 @@ export interface AutoPlayDetectionResult {
364
447
  pauseVerification: PauseVerificationResult;
365
448
  recommendation: string;
366
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;