@masup9/a11y-audit 0.1.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 (36) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.ja.md +128 -0
  3. package/README.md +130 -0
  4. package/dist/constants.d.ts +88 -0
  5. package/dist/constants.js +184 -0
  6. package/dist/index.d.ts +13 -0
  7. package/dist/index.js +13 -0
  8. package/dist/playwright/index.d.ts +19 -0
  9. package/dist/playwright/index.js +19 -0
  10. package/dist/playwright/runAxeAudit.d.ts +28 -0
  11. package/dist/playwright/runAxeAudit.js +89 -0
  12. package/dist/playwright/runFocusIndicatorCheck.d.ts +30 -0
  13. package/dist/playwright/runFocusIndicatorCheck.js +740 -0
  14. package/dist/playwright/runReflowCheck.d.ts +36 -0
  15. package/dist/playwright/runReflowCheck.js +68 -0
  16. package/dist/playwright/runTargetSizeCheck.d.ts +35 -0
  17. package/dist/playwright/runTargetSizeCheck.js +389 -0
  18. package/dist/schemas/index.d.ts +34 -0
  19. package/dist/schemas/index.js +245 -0
  20. package/dist/test-entries/axe-audit.d.ts +13 -0
  21. package/dist/test-entries/axe-audit.js +20 -0
  22. package/dist/test-entries/focus-indicator-check.d.ts +9 -0
  23. package/dist/test-entries/focus-indicator-check.js +13 -0
  24. package/dist/test-entries/reflow-check.d.ts +9 -0
  25. package/dist/test-entries/reflow-check.js +21 -0
  26. package/dist/test-entries/target-size-check.d.ts +8 -0
  27. package/dist/test-entries/target-size-check.js +15 -0
  28. package/dist/types.d.ts +215 -0
  29. package/dist/types.js +4 -0
  30. package/dist/utils/annotations.d.ts +67 -0
  31. package/dist/utils/annotations.js +110 -0
  32. package/dist/utils/layout.d.ts +23 -0
  33. package/dist/utils/layout.js +144 -0
  34. package/dist/utils/test-harness.d.ts +76 -0
  35. package/dist/utils/test-harness.js +119 -0
  36. package/package.json +92 -0
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Reflow Check — WCAG 1.4.10 (Reflow)
3
+ *
4
+ * Sets the viewport to a narrow width (default 320px, equivalent to 400% zoom
5
+ * on a 1280px viewport), then detects horizontal scrolling, overflowing
6
+ * elements, and clipped text.
7
+ *
8
+ * The caller is responsible for navigating the page before calling this
9
+ * function. This function sets the viewport size itself so the measurement is
10
+ * taken at the reflow width regardless of how the page was loaded.
11
+ *
12
+ * Limitations:
13
+ * - Cannot distinguish acceptable horizontal scroll (e.g., data tables)
14
+ * - Does not verify functional reflow for complex widgets
15
+ */
16
+ import type { Page } from '@playwright/test';
17
+ import type { ReflowCheckResult } from '../types.js';
18
+ import { type OutputLocationOptions } from '../utils/test-harness.js';
19
+ export interface RunReflowCheckOptions extends OutputLocationOptions {
20
+ /** A page already navigated to the target URL. */
21
+ page: Page;
22
+ /** Viewport to measure reflow at (default: 320x256). */
23
+ viewport?: {
24
+ width: number;
25
+ height: number;
26
+ };
27
+ /** Overflow tolerance in pixels (default: 5). */
28
+ overflowTolerance?: number;
29
+ /** Whether to capture a screenshot next to the result file (default: false). */
30
+ screenshot?: boolean;
31
+ }
32
+ /**
33
+ * Run the reflow check against the current page, write the result JSON
34
+ * (and optionally a screenshot), and return the parsed result.
35
+ */
36
+ export declare function runReflowCheck(options: RunReflowCheckOptions): Promise<ReflowCheckResult>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Reflow Check — WCAG 1.4.10 (Reflow)
3
+ *
4
+ * Sets the viewport to a narrow width (default 320px, equivalent to 400% zoom
5
+ * on a 1280px viewport), then detects horizontal scrolling, overflowing
6
+ * elements, and clipped text.
7
+ *
8
+ * The caller is responsible for navigating the page before calling this
9
+ * function. This function sets the viewport size itself so the measurement is
10
+ * taken at the reflow width regardless of how the page was loaded.
11
+ *
12
+ * Limitations:
13
+ * - Cannot distinguish acceptable horizontal scroll (e.g., data tables)
14
+ * - Does not verify functional reflow for complex widgets
15
+ */
16
+ import { REFLOW_VIEWPORT, REFLOW_OVERFLOW_TOLERANCE, REFLOW_CHECK_SELECTOR, REFLOW_ALLOWED_OVERFLOW_SELECTORS, DEFAULT_REFLOW_RESULT_FILE, DEFAULT_REFLOW_SCREENSHOT_FILE, } from '../constants.js';
17
+ import { createLayoutChecker } from '../utils/layout.js';
18
+ import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
19
+ /**
20
+ * Run the reflow check against the current page, write the result JSON
21
+ * (and optionally a screenshot), and return the parsed result.
22
+ */
23
+ export async function runReflowCheck(options) {
24
+ const { page, viewport = REFLOW_VIEWPORT, overflowTolerance = REFLOW_OVERFLOW_TOLERANCE, screenshot = false, ...location } = options;
25
+ await page.setViewportSize({ width: viewport.width, height: viewport.height });
26
+ const layoutResult = await page.evaluate(createLayoutChecker, {
27
+ viewportWidth: viewport.width,
28
+ overflowTolerance,
29
+ checkSelector: REFLOW_CHECK_SELECTOR,
30
+ allowedOverflowSelectors: [...REFLOW_ALLOWED_OVERFLOW_SELECTORS],
31
+ });
32
+ const result = {
33
+ url: page.url(),
34
+ viewport: { width: viewport.width, height: viewport.height },
35
+ ...layoutResult,
36
+ };
37
+ // Output results
38
+ logAuditHeader('Reflow Check Results', 'WCAG 1.4.10', result.url);
39
+ logSummary({
40
+ Viewport: `${result.viewport.width}x${result.viewport.height}`,
41
+ 'Document scroll width': `${result.documentScrollWidth}px`,
42
+ 'Document client width': `${result.documentClientWidth}px`,
43
+ 'Horizontal scroll': result.hasHorizontalScroll,
44
+ 'Overflowing elements': result.overflowingElements.length,
45
+ 'Clipped text elements': result.clippedTextElements.length,
46
+ });
47
+ logIssueList('Overflowing Elements', result.overflowingElements, (el, i) => [
48
+ `${i + 1}. <${el.tagName}> "${el.selector}"`,
49
+ ` rect.right: ${el.rect.right}px (viewport: ${el.viewportWidth}px)`,
50
+ ]);
51
+ logIssueList('Clipped Text Elements', result.clippedTextElements, (el, i) => [
52
+ `${i + 1}. <${el.tagName}> "${el.selector}"`,
53
+ ` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
54
+ ` overflow: ${el.overflow}, overflowX: ${el.overflowX}`,
55
+ ]);
56
+ const resolvedPath = saveAuditResult(result, {
57
+ ...location,
58
+ defaultFile: DEFAULT_REFLOW_RESULT_FILE,
59
+ });
60
+ let screenshotPath;
61
+ if (screenshot) {
62
+ screenshotPath = await takeAuditScreenshot(page, {
63
+ path: resolveScreenshotPath(resolvedPath, DEFAULT_REFLOW_SCREENSHOT_FILE),
64
+ });
65
+ }
66
+ logOutputPaths(resolvedPath, screenshotPath);
67
+ return result;
68
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Target Size Check
3
+ *
4
+ * WCAG 2.5.8 (Target Size Minimum - AA): 24x24 CSS px
5
+ * WCAG 2.5.5 (Target Size Enhanced - AAA): 44x44 CSS px
6
+ *
7
+ * The caller is responsible for navigating the page before calling this
8
+ * function.
9
+ *
10
+ * Note: when `screenshot` is enabled this function appends an annotation
11
+ * overlay to the page DOM before capturing the screenshot.
12
+ *
13
+ * Limitations:
14
+ * - Essential exception requires manual review
15
+ * - Cannot detect all redundant target cases
16
+ * - CSS transform may affect measurements
17
+ */
18
+ import type { Page } from '@playwright/test';
19
+ import type { TargetSizeCheckResult } from '../types.js';
20
+ import { type OutputLocationOptions } from '../utils/test-harness.js';
21
+ export interface RunTargetSizeCheckOptions extends OutputLocationOptions {
22
+ /** A page already navigated to the target URL. */
23
+ page: Page;
24
+ /** Minimum (AA) threshold in CSS px (default: 24). */
25
+ aaThreshold?: number;
26
+ /** Enhanced (AAA) threshold in CSS px (default: 44). */
27
+ aaaThreshold?: number;
28
+ /** Whether to capture an annotated screenshot (default: false). Mutates the page DOM. */
29
+ screenshot?: boolean;
30
+ }
31
+ /**
32
+ * Run the target size check against the current page, write the result JSON
33
+ * (and optionally an annotated screenshot), and return the parsed result.
34
+ */
35
+ export declare function runTargetSizeCheck(options: RunTargetSizeCheckOptions): Promise<TargetSizeCheckResult>;
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Target Size Check
3
+ *
4
+ * WCAG 2.5.8 (Target Size Minimum - AA): 24x24 CSS px
5
+ * WCAG 2.5.5 (Target Size Enhanced - AAA): 44x44 CSS px
6
+ *
7
+ * The caller is responsible for navigating the page before calling this
8
+ * function.
9
+ *
10
+ * Note: when `screenshot` is enabled this function appends an annotation
11
+ * overlay to the page DOM before capturing the screenshot.
12
+ *
13
+ * Limitations:
14
+ * - Essential exception requires manual review
15
+ * - Cannot detect all redundant target cases
16
+ * - CSS transform may affect measurements
17
+ */
18
+ import { INTERACTIVE_SELECTOR, TARGET_SIZE_AA, TARGET_SIZE_AAA, INLINE_CONTEXT_TAGS, UA_CONTROLLED_INPUT_TYPES, INLINE_CONTEXT_MIN_TEXT, DEFAULT_TARGET_SIZE_RESULT_FILE, DEFAULT_TARGET_SIZE_SCREENSHOT_FILE, } from '../constants.js';
19
+ import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
20
+ import { addPageAnnotations } from '../utils/annotations.js';
21
+ /**
22
+ * Collect basic target information from DOM (runs in browser context).
23
+ */
24
+ function collectBasicTargetInfo(interactiveSelector) {
25
+ function getUniqueSelector(element, elementIndex) {
26
+ if (element.id) {
27
+ return `#${CSS.escape(element.id)}`;
28
+ }
29
+ const path = [];
30
+ let current = element;
31
+ while (current && current !== document.body) {
32
+ let selector = current.tagName.toLowerCase();
33
+ const parent = current.parentElement;
34
+ if (parent) {
35
+ const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
36
+ if (siblings.length > 1) {
37
+ const index = siblings.indexOf(current) + 1;
38
+ selector += `:nth-of-type(${index})`;
39
+ }
40
+ }
41
+ path.unshift(selector);
42
+ current = parent;
43
+ }
44
+ return path.length > 0 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
45
+ }
46
+ const targets = [];
47
+ const elements = document.querySelectorAll(interactiveSelector);
48
+ elements.forEach((element, index) => {
49
+ const el = element;
50
+ const rect = el.getBoundingClientRect();
51
+ // Skip invisible elements
52
+ if (rect.width === 0 || rect.height === 0) {
53
+ return;
54
+ }
55
+ // Skip elements outside viewport (likely hidden)
56
+ if (rect.bottom < 0 || rect.right < 0) {
57
+ return;
58
+ }
59
+ const computedStyle = getComputedStyle(el);
60
+ if (computedStyle.visibility === 'hidden' ||
61
+ computedStyle.display === 'none') {
62
+ return;
63
+ }
64
+ const tagName = el.tagName.toLowerCase();
65
+ const role = el.getAttribute('role');
66
+ const inputType = el instanceof HTMLInputElement ? el.type : null;
67
+ const href = el instanceof HTMLAnchorElement ? el.href : null;
68
+ // Get parent info for inline exception check
69
+ const parent = el.parentElement;
70
+ const parentTag = parent ? parent.tagName.toLowerCase() : null;
71
+ const parentTextLength = parent ? (parent.textContent || '').length : 0;
72
+ targets.push({
73
+ selector: getUniqueSelector(element, index),
74
+ tagName,
75
+ role,
76
+ width: Math.round(rect.width * 100) / 100,
77
+ height: Math.round(rect.height * 100) / 100,
78
+ href,
79
+ inputType,
80
+ appearance: computedStyle.appearance,
81
+ parentTag,
82
+ parentTextLength,
83
+ boundingRect: {
84
+ left: rect.left,
85
+ top: rect.top,
86
+ right: rect.right,
87
+ bottom: rect.bottom,
88
+ },
89
+ });
90
+ });
91
+ return targets;
92
+ }
93
+ /**
94
+ * Parse accessible name from ariaSnapshot output.
95
+ */
96
+ function parseAccessibleName(snapshot) {
97
+ // ariaSnapshot format: "- role \"accessible name\"" or "- role \"accessible name\" [state]"
98
+ const match = snapshot.match(/^- \w+(?:\s+"([^"]*)")?/);
99
+ if (match && match[1]) {
100
+ return match[1];
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Check if element qualifies for inline exception.
106
+ */
107
+ function checkInlineException(target, inlineContextTags, minTextLength) {
108
+ // Only links typically qualify for inline exception
109
+ if (target.tagName !== 'a' || !target.href) {
110
+ return { applies: false, details: null };
111
+ }
112
+ if (target.parentTag &&
113
+ inlineContextTags.includes(target.parentTag) &&
114
+ target.parentTextLength >= minTextLength) {
115
+ return {
116
+ applies: true,
117
+ details: `Inline link within <${target.parentTag}>, surrounding text: ${target.parentTextLength} chars`,
118
+ };
119
+ }
120
+ return { applies: false, details: null };
121
+ }
122
+ /**
123
+ * Check if element qualifies for UA control exception.
124
+ */
125
+ function checkUAControlException(target, uaControlledTypes) {
126
+ // Check native form controls that haven't been restyled
127
+ if (target.tagName === 'select') {
128
+ if (target.appearance !== 'none') {
129
+ return {
130
+ applies: true,
131
+ details: 'Native <select> element with default appearance',
132
+ };
133
+ }
134
+ }
135
+ if (target.tagName === 'input' &&
136
+ target.inputType &&
137
+ uaControlledTypes.includes(target.inputType)) {
138
+ if (target.appearance !== 'none') {
139
+ return {
140
+ applies: true,
141
+ details: `Native <input type="${target.inputType}"> with default appearance`,
142
+ };
143
+ }
144
+ }
145
+ return { applies: false, details: null };
146
+ }
147
+ /**
148
+ * Check for redundant targets (same href with at least one meeting size).
149
+ */
150
+ function findRedundantTargets(targets) {
151
+ const byHref = new Map();
152
+ for (const target of targets) {
153
+ if (target.href) {
154
+ const existing = byHref.get(target.href) || [];
155
+ existing.push(target);
156
+ byHref.set(target.href, existing);
157
+ }
158
+ }
159
+ return byHref;
160
+ }
161
+ /**
162
+ * Check if target has adequate spacing from adjacent targets.
163
+ */
164
+ function checkSpacingException(target, allTargets, requiredSpacing) {
165
+ const targetRect = target.boundingRect;
166
+ for (const other of allTargets) {
167
+ if (other.selector === target.selector) {
168
+ continue;
169
+ }
170
+ const otherRect = other.boundingRect;
171
+ // Calculate distance between edges
172
+ const horizontalGap = Math.max(0, Math.max(otherRect.left - targetRect.right, targetRect.left - otherRect.right));
173
+ const verticalGap = Math.max(0, Math.max(otherRect.top - targetRect.bottom, targetRect.top - otherRect.bottom));
174
+ // If adjacent (gaps are small), check if spacing is adequate
175
+ if (horizontalGap < requiredSpacing && verticalGap < requiredSpacing) {
176
+ // Not enough spacing
177
+ return { applies: false, details: null };
178
+ }
179
+ }
180
+ // All adjacent targets have adequate spacing
181
+ return {
182
+ applies: true,
183
+ details: `No adjacent targets within ${requiredSpacing}px`,
184
+ };
185
+ }
186
+ /**
187
+ * Analyze targets and categorize by pass/fail level.
188
+ */
189
+ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
190
+ const failAA = [];
191
+ const failAAAOnly = [];
192
+ const excepted = [];
193
+ let passCount = 0;
194
+ // Build href map for redundancy check
195
+ const hrefMap = findRedundantTargets(targets);
196
+ for (const target of targets) {
197
+ const minDimension = Math.min(target.width, target.height);
198
+ // Determine level
199
+ let level;
200
+ if (minDimension >= aaaThreshold) {
201
+ passCount++;
202
+ continue;
203
+ }
204
+ else if (minDimension >= aaThreshold) {
205
+ level = 'fail-aaa-only';
206
+ }
207
+ else {
208
+ level = 'fail-aa';
209
+ }
210
+ // Check exceptions (only relevant for fail-aa)
211
+ let exception = null;
212
+ let exceptionDetails = null;
213
+ if (level === 'fail-aa') {
214
+ // Check inline exception
215
+ const inlineCheck = checkInlineException(target, INLINE_CONTEXT_TAGS, INLINE_CONTEXT_MIN_TEXT);
216
+ if (inlineCheck.applies) {
217
+ exception = 'inline';
218
+ exceptionDetails = inlineCheck.details;
219
+ }
220
+ // Check UA control exception
221
+ if (!exception) {
222
+ const uaCheck = checkUAControlException(target, UA_CONTROLLED_INPUT_TYPES);
223
+ if (uaCheck.applies) {
224
+ exception = 'ua-control';
225
+ exceptionDetails = uaCheck.details;
226
+ }
227
+ }
228
+ // Check redundant exception
229
+ if (!exception && target.href) {
230
+ const sameHrefTargets = hrefMap.get(target.href) || [];
231
+ const hasLargerTarget = sameHrefTargets.some((t) => t.selector !== target.selector &&
232
+ Math.min(t.width, t.height) >= aaThreshold);
233
+ if (hasLargerTarget) {
234
+ exception = 'redundant';
235
+ exceptionDetails = `Another link to same URL meets size requirement`;
236
+ }
237
+ }
238
+ // Check spacing exception
239
+ if (!exception) {
240
+ const spacingCheck = checkSpacingException(target, targets, aaThreshold);
241
+ if (spacingCheck.applies) {
242
+ exception = 'spacing';
243
+ exceptionDetails = spacingCheck.details;
244
+ }
245
+ }
246
+ }
247
+ const issue = {
248
+ selector: target.selector,
249
+ tagName: target.tagName,
250
+ role: target.role,
251
+ accessibleName: target.accessibleName,
252
+ width: target.width,
253
+ height: target.height,
254
+ minDimension: Math.round(minDimension * 100) / 100,
255
+ level,
256
+ exception,
257
+ exceptionDetails,
258
+ href: target.href,
259
+ };
260
+ if (exception) {
261
+ excepted.push(issue);
262
+ }
263
+ else if (level === 'fail-aa') {
264
+ failAA.push(issue);
265
+ }
266
+ else {
267
+ failAAAOnly.push(issue);
268
+ }
269
+ }
270
+ return { failAA, failAAAOnly, passCount, excepted };
271
+ }
272
+ /**
273
+ * Run the target size check against the current page, write the result JSON
274
+ * (and optionally an annotated screenshot), and return the parsed result.
275
+ */
276
+ export async function runTargetSizeCheck(options) {
277
+ const { page, aaThreshold = TARGET_SIZE_AA, aaaThreshold = TARGET_SIZE_AAA, screenshot = false, ...location } = options;
278
+ // Collect basic target info from DOM
279
+ const basicTargets = await page.evaluate(collectBasicTargetInfo, INTERACTIVE_SELECTOR);
280
+ // Enhance with accessible names via ariaSnapshot()
281
+ const targets = [];
282
+ for (const basicTarget of basicTargets) {
283
+ let accessibleName = null;
284
+ try {
285
+ const locator = page.locator(basicTarget.selector).first();
286
+ const snapshot = await locator.ariaSnapshot();
287
+ accessibleName = parseAccessibleName(snapshot);
288
+ }
289
+ catch {
290
+ // If ariaSnapshot fails, accessibleName remains null
291
+ }
292
+ targets.push({
293
+ ...basicTarget,
294
+ accessibleName,
295
+ });
296
+ }
297
+ // Analyze targets
298
+ const { failAA, failAAAOnly, passCount, excepted } = analyzeTargets(targets, aaThreshold, aaaThreshold);
299
+ const result = {
300
+ url: page.url(),
301
+ totalTargetsChecked: targets.length,
302
+ failAA,
303
+ failAAAOnly,
304
+ passedTargets: passCount,
305
+ exceptedTargets: excepted,
306
+ summary: {
307
+ failAACount: failAA.length,
308
+ failAAAOnlyCount: failAAAOnly.length,
309
+ passCount,
310
+ exceptedCount: excepted.length,
311
+ },
312
+ };
313
+ // Output results
314
+ logAuditHeader('Target Size Check Results', 'WCAG 2.5.5 / 2.5.8', result.url);
315
+ logSummary({
316
+ 'Total targets checked': result.totalTargetsChecked,
317
+ });
318
+ console.log('\nSummary:');
319
+ console.log(` Pass (>= ${aaaThreshold}px): ${result.summary.passCount}`);
320
+ console.log(` Fail AAA only (${aaThreshold}-${aaaThreshold - 1}px): ${result.summary.failAAAOnlyCount}`);
321
+ console.log(` Fail AA (< ${aaThreshold}px): ${result.summary.failAACount}`);
322
+ console.log(` Possible exceptions: ${result.summary.exceptedCount}`);
323
+ logIssueList(`Fail AA (< ${aaThreshold}px) - Requires Fix`, failAA, (el, i) => {
324
+ const lines = [
325
+ `${i + 1}. <${el.tagName}> "${el.selector}"`,
326
+ ` Size: ${el.width}x${el.height}px (min: ${el.minDimension}px)`,
327
+ ` Name: "${el.accessibleName || 'none'}"`,
328
+ ];
329
+ if (el.exception) {
330
+ lines.push(` Exception: ${el.exception} - ${el.exceptionDetails}`);
331
+ }
332
+ return lines;
333
+ });
334
+ logIssueList(`Fail AAA Only (${aaThreshold}-${aaaThreshold - 1}px) - Recommended Fix`, failAAAOnly, (el, i) => [
335
+ `${i + 1}. <${el.tagName}> "${el.selector}"`,
336
+ ` Size: ${el.width}x${el.height}px (min: ${el.minDimension}px)`,
337
+ ` Name: "${el.accessibleName || 'none'}"`,
338
+ ], 5);
339
+ logIssueList('Possible Exceptions (Manual Review Recommended)', excepted, (el, i) => [
340
+ `${i + 1}. <${el.tagName}> "${el.selector}"`,
341
+ ` Size: ${el.width}x${el.height}px (min: ${el.minDimension}px)`,
342
+ ` Exception: ${el.exception} - ${el.exceptionDetails}`,
343
+ ], 5);
344
+ const resolvedPath = saveAuditResult(result, {
345
+ ...location,
346
+ defaultFile: DEFAULT_TARGET_SIZE_RESULT_FILE,
347
+ });
348
+ let screenshotPath;
349
+ if (screenshot) {
350
+ // Build annotations for screenshot
351
+ const passSelectors = targets
352
+ .filter((t) => Math.min(t.width, t.height) >= aaaThreshold)
353
+ .map((t) => t.selector);
354
+ const annotations = [
355
+ ...passSelectors.map((s) => ({
356
+ selector: s,
357
+ label: 'PASS',
358
+ colorScheme: 'pass',
359
+ })),
360
+ ...failAAAOnly.map((t) => ({
361
+ selector: t.selector,
362
+ label: 'AA Pass',
363
+ colorScheme: 'warning',
364
+ })),
365
+ ...failAA.map((t) => ({
366
+ selector: t.selector,
367
+ label: 'AA Fail',
368
+ colorScheme: 'fail',
369
+ })),
370
+ ...excepted.map((t) => ({
371
+ selector: t.selector,
372
+ label: 'Exception',
373
+ colorScheme: 'info',
374
+ })),
375
+ ];
376
+ await addPageAnnotations(page, annotations);
377
+ screenshotPath = await takeAuditScreenshot(page, {
378
+ path: resolveScreenshotPath(resolvedPath, DEFAULT_TARGET_SIZE_SCREENSHOT_FILE),
379
+ });
380
+ // Legend
381
+ console.log('\nLegend:');
382
+ console.log(` PASS (green): >= ${aaaThreshold}px - AA Pass, AAA Pass`);
383
+ console.log(` AA Pass (orange): ${aaThreshold}-${aaaThreshold - 1}px - AA Pass, AAA Fail`);
384
+ console.log(` AA Fail (red): < ${aaThreshold}px - AA Fail, AAA Fail`);
385
+ console.log(` Exception (blue): Possible exception (manual review needed)`);
386
+ }
387
+ logOutputPaths(resolvedPath, screenshotPath);
388
+ return result;
389
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Result types and JSON Schemas for the audit checks.
3
+ *
4
+ * The TypeScript types are the source of truth; the JSON Schemas are
5
+ * hand-written for consumers that validate the result files at runtime
6
+ * (e.g. an issue creator that reads `*-result.json`). They are intentionally
7
+ * permissive (no `additionalProperties: false`) so that additive changes to a
8
+ * result shape do not break downstream validation.
9
+ */
10
+ export type { AxeViolationNode, AxeViolation, AxeAuditResult, FocusRecord, OnFocusViolation, FocusCheckResult, BoundingRect, FocusObscuredOverlap, FocusObscuredIssue, ReflowIssue, ClippedTextElement, ReflowCheckResult, TargetSizeException, TargetSizeIssue, TargetSizeSummary, TargetSizeCheckResult, } from '../types.js';
11
+ /** Minimal JSON Schema object shape (Draft 2020-12 compatible subset). */
12
+ export interface JsonSchema {
13
+ $schema?: string;
14
+ $id?: string;
15
+ title?: string;
16
+ type?: string | string[];
17
+ properties?: Record<string, JsonSchema>;
18
+ items?: JsonSchema;
19
+ required?: string[];
20
+ enum?: unknown[];
21
+ description?: string;
22
+ [key: string]: unknown;
23
+ }
24
+ export declare const AXE_AUDIT_RESULT_SCHEMA: JsonSchema;
25
+ export declare const FOCUS_CHECK_RESULT_SCHEMA: JsonSchema;
26
+ export declare const REFLOW_CHECK_RESULT_SCHEMA: JsonSchema;
27
+ export declare const TARGET_SIZE_CHECK_RESULT_SCHEMA: JsonSchema;
28
+ /** All result schemas keyed by check id. */
29
+ export declare const RESULT_SCHEMAS: {
30
+ readonly 'axe-audit': JsonSchema;
31
+ readonly 'focus-indicator-check': JsonSchema;
32
+ readonly 'reflow-check': JsonSchema;
33
+ readonly 'target-size-check': JsonSchema;
34
+ };