@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,40 @@
1
+ /**
2
+ * Orientation Check — WCAG 1.3.4 (Orientation)
3
+ *
4
+ * Renders the page in portrait (375x667) and landscape (667x375), detecting
5
+ * "rotate device" messages/overlays and whether the main content is hidden in
6
+ * either orientation. Reports if the page restricts content to a specific
7
+ * orientation.
8
+ *
9
+ * This function OWNS navigation: it sets each viewport then navigates to the
10
+ * target URL for that orientation (two full navigations, not a reload), so the
11
+ * page lays out fresh for each orientation.
12
+ *
13
+ * Limitations:
14
+ * - Heuristics may miss CSS-only orientation restrictions
15
+ * - Cannot detect JavaScript-based orientation detection without visual indicators
16
+ * - Manual verification needed for exceptions (e.g., camera apps)
17
+ */
18
+ import type { Page } from '@playwright/test';
19
+ import type { OrientationCheckResult } from '../types.js';
20
+ import { type OutputLocationOptions } from '../utils/test-harness.js';
21
+ export interface RunOrientationCheckOptions extends OutputLocationOptions {
22
+ /**
23
+ * A page to drive. This check navigates the page itself (once per
24
+ * orientation), so the page does not need to be pre-navigated.
25
+ */
26
+ page: Page;
27
+ /** Target URL to audit. Falls back to the `TEST_PAGE` env var. */
28
+ targetUrl?: string;
29
+ /**
30
+ * Whether to capture portrait/landscape screenshots next to the result file
31
+ * (default: false).
32
+ */
33
+ screenshot?: boolean;
34
+ }
35
+ /**
36
+ * Run the orientation check, navigating the page in both portrait and landscape,
37
+ * write the result JSON (and optionally screenshots), and return the parsed
38
+ * result.
39
+ */
40
+ export declare function runOrientationCheck(options: RunOrientationCheckOptions): Promise<OrientationCheckResult>;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Orientation Check — WCAG 1.3.4 (Orientation)
3
+ *
4
+ * Renders the page in portrait (375x667) and landscape (667x375), detecting
5
+ * "rotate device" messages/overlays and whether the main content is hidden in
6
+ * either orientation. Reports if the page restricts content to a specific
7
+ * orientation.
8
+ *
9
+ * This function OWNS navigation: it sets each viewport then navigates to the
10
+ * target URL for that orientation (two full navigations, not a reload), so the
11
+ * page lays out fresh for each orientation.
12
+ *
13
+ * Limitations:
14
+ * - Heuristics may miss CSS-only orientation restrictions
15
+ * - Cannot detect JavaScript-based orientation detection without visual indicators
16
+ * - Manual verification needed for exceptions (e.g., camera apps)
17
+ */
18
+ import { ORIENTATION_VIEWPORTS, ORIENTATION_LOCK_KEYWORDS, MAIN_CONTENT_SELECTORS, DEFAULT_ORIENTATION_RESULT_FILE, DEFAULT_ORIENTATION_PORTRAIT_SCREENSHOT_FILE, DEFAULT_ORIENTATION_LANDSCAPE_SCREENSHOT_FILE, } from '../constants.js';
19
+ import { buildAuditResult, normalizeOrientationCheck, } from '../utils/axe-format.js';
20
+ import { saveAuditResult, resolveOutputPath, takeAuditScreenshot, resolveScreenshotPath, requireTargetUrl, logAuditHeader, logOutputPaths, } from '../utils/test-harness.js';
21
+ /** Capture orientation state in browser context. */
22
+ function captureOrientationState(args) {
23
+ const { lockKeywords, mainContentSelectors } = args;
24
+ const bodyWidth = document.body.scrollWidth;
25
+ const bodyHeight = document.body.scrollHeight;
26
+ const visibleText = document.body.innerText || '';
27
+ const visibleTextLength = visibleText.length;
28
+ const lowerText = visibleText.toLowerCase();
29
+ // Search for lock message keywords
30
+ let lockMessageFound = false;
31
+ let lockMessageText = null;
32
+ for (const keyword of lockKeywords) {
33
+ if (lowerText.includes(keyword.toLowerCase())) {
34
+ lockMessageFound = true;
35
+ const allElements = document.querySelectorAll('*');
36
+ for (const el of allElements) {
37
+ const text = el.textContent?.toLowerCase() || '';
38
+ if (text.includes(keyword.toLowerCase()) && text.length < 200) {
39
+ lockMessageText = el.textContent?.trim().slice(0, 100) || null;
40
+ break;
41
+ }
42
+ }
43
+ break;
44
+ }
45
+ }
46
+ // Check if main content is hidden
47
+ let mainContentHidden = false;
48
+ for (const selector of mainContentSelectors) {
49
+ const mainEl = document.querySelector(selector);
50
+ if (mainEl) {
51
+ const style = window.getComputedStyle(mainEl);
52
+ const isHidden = style.display === 'none' ||
53
+ style.visibility === 'hidden' ||
54
+ parseFloat(style.opacity) === 0;
55
+ if (isHidden) {
56
+ mainContentHidden = true;
57
+ break;
58
+ }
59
+ const rect = mainEl.getBoundingClientRect();
60
+ if (rect.width < 50 || rect.height < 50) {
61
+ mainContentHidden = true;
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ return {
67
+ lockMessageFound,
68
+ lockMessageText,
69
+ mainContentHidden,
70
+ bodyWidth,
71
+ bodyHeight,
72
+ visibleTextLength,
73
+ };
74
+ }
75
+ /** Determine where orientation lock was detected. */
76
+ function determineLockLocation(portraitHasLock, landscapeHasLock) {
77
+ if (portraitHasLock && landscapeHasLock) {
78
+ return 'both';
79
+ }
80
+ if (portraitHasLock) {
81
+ return 'portrait';
82
+ }
83
+ if (landscapeHasLock) {
84
+ return 'landscape';
85
+ }
86
+ return 'none';
87
+ }
88
+ /** Log orientation state for a specific orientation. */
89
+ function logOrientationState(label, viewport, state) {
90
+ console.log(`\n${label} (${viewport.width}x${viewport.height}):`);
91
+ console.log(` Lock message found: ${state.lockMessageFound ? 'YES' : 'No'}`);
92
+ if (state.lockMessageText) {
93
+ console.log(` Message: "${state.lockMessageText}"`);
94
+ }
95
+ console.log(` Main content hidden: ${state.mainContentHidden ? 'YES' : 'No'}`);
96
+ console.log(` Body size: ${state.bodyWidth}x${state.bodyHeight}`);
97
+ }
98
+ /**
99
+ * Run the orientation check, navigating the page in both portrait and landscape,
100
+ * write the result JSON (and optionally screenshots), and return the parsed
101
+ * result.
102
+ */
103
+ export async function runOrientationCheck(options) {
104
+ const { page, targetUrl, screenshot = false, ...location } = options;
105
+ const url = requireTargetUrl(targetUrl);
106
+ const checkArgs = {
107
+ lockKeywords: [...ORIENTATION_LOCK_KEYWORDS],
108
+ mainContentSelectors: [...MAIN_CONTENT_SELECTORS],
109
+ };
110
+ // Resolve where the result will be written up front so screenshots can be
111
+ // placed next to it (mirrors saveAuditResult's resolution).
112
+ const resolvedPath = resolveOutputPath({
113
+ ...location,
114
+ defaultFile: DEFAULT_ORIENTATION_RESULT_FILE,
115
+ });
116
+ // Test portrait orientation
117
+ await page.setViewportSize(ORIENTATION_VIEWPORTS.portrait);
118
+ await page.goto(url, { waitUntil: 'networkidle' });
119
+ const portraitState = await page.evaluate(captureOrientationState, checkArgs);
120
+ let portraitScreenshotPath;
121
+ if (screenshot) {
122
+ portraitScreenshotPath = await takeAuditScreenshot(page, {
123
+ path: resolveScreenshotPath(resolvedPath, DEFAULT_ORIENTATION_PORTRAIT_SCREENSHOT_FILE),
124
+ });
125
+ }
126
+ // Test landscape orientation
127
+ await page.setViewportSize(ORIENTATION_VIEWPORTS.landscape);
128
+ await page.goto(url, { waitUntil: 'networkidle' });
129
+ const landscapeState = await page.evaluate(captureOrientationState, checkArgs);
130
+ let landscapeScreenshotPath;
131
+ if (screenshot) {
132
+ landscapeScreenshotPath = await takeAuditScreenshot(page, {
133
+ path: resolveScreenshotPath(resolvedPath, DEFAULT_ORIENTATION_LANDSCAPE_SCREENSHOT_FILE),
134
+ });
135
+ }
136
+ // Determine lock status
137
+ const portraitHasLock = portraitState.lockMessageFound || portraitState.mainContentHidden;
138
+ const landscapeHasLock = landscapeState.lockMessageFound || landscapeState.mainContentHidden;
139
+ const hasOrientationLock = portraitHasLock || landscapeHasLock;
140
+ const lockDetectedIn = determineLockLocation(portraitHasLock, landscapeHasLock);
141
+ const details = {
142
+ portrait: portraitState,
143
+ landscape: landscapeState,
144
+ hasOrientationLock,
145
+ lockDetectedIn,
146
+ };
147
+ const result = buildAuditResult({
148
+ source: 'orientation-check',
149
+ url: page.url(),
150
+ details,
151
+ buckets: normalizeOrientationCheck(details),
152
+ });
153
+ // Output results
154
+ logAuditHeader('Orientation Check Results', 'WCAG 1.3.4', result.url);
155
+ logOrientationState('Portrait', ORIENTATION_VIEWPORTS.portrait, details.portrait);
156
+ logOrientationState('Landscape', ORIENTATION_VIEWPORTS.landscape, details.landscape);
157
+ console.log(`\nOrientation lock detected: ${details.hasOrientationLock ? 'YES' : 'No'}`);
158
+ if (details.hasOrientationLock) {
159
+ console.log(`Lock detected in: ${details.lockDetectedIn}`);
160
+ }
161
+ const writtenPath = saveAuditResult(result, {
162
+ ...location,
163
+ defaultFile: DEFAULT_ORIENTATION_RESULT_FILE,
164
+ });
165
+ const screenshotPaths = [portraitScreenshotPath, landscapeScreenshotPath]
166
+ .filter((p) => p !== undefined)
167
+ .join(', ');
168
+ logOutputPaths(writtenPath, screenshotPaths || undefined);
169
+ return result;
170
+ }
@@ -13,8 +13,9 @@
13
13
  * - Cannot distinguish acceptable horizontal scroll (e.g., data tables)
14
14
  * - Does not verify functional reflow for complex widgets
15
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';
16
+ import { REFLOW_VIEWPORT, REFLOW_OVERFLOW_TOLERANCE, REFLOW_CHECK_SELECTOR, REFLOW_ALLOWED_OVERFLOW_SELECTORS, DEFAULT_REFLOW_RESULT_FILE, DEFAULT_REFLOW_SCREENSHOT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
17
17
  import { createLayoutChecker } from '../utils/layout.js';
18
+ import { buildAuditResult, normalizeReflowCheck } from '../utils/axe-format.js';
18
19
  import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
19
20
  /**
20
21
  * Run the reflow check against the current page, write the result JSON
@@ -28,27 +29,33 @@ export async function runReflowCheck(options) {
28
29
  overflowTolerance,
29
30
  checkSelector: REFLOW_CHECK_SELECTOR,
30
31
  allowedOverflowSelectors: [...REFLOW_ALLOWED_OVERFLOW_SELECTORS],
32
+ htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
31
33
  });
32
- const result = {
33
- url: page.url(),
34
+ const details = {
34
35
  viewport: { width: viewport.width, height: viewport.height },
35
36
  ...layoutResult,
36
37
  };
38
+ const result = buildAuditResult({
39
+ source: 'reflow-check',
40
+ url: page.url(),
41
+ details,
42
+ buckets: normalizeReflowCheck(details),
43
+ });
37
44
  // Output results
38
45
  logAuditHeader('Reflow Check Results', 'WCAG 1.4.10', result.url);
39
46
  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,
47
+ Viewport: `${details.viewport.width}x${details.viewport.height}`,
48
+ 'Document scroll width': `${details.documentScrollWidth}px`,
49
+ 'Document client width': `${details.documentClientWidth}px`,
50
+ 'Horizontal scroll': details.hasHorizontalScroll,
51
+ 'Overflowing elements': details.overflowingElements.length,
52
+ 'Clipped text elements': details.clippedTextElements.length,
46
53
  });
47
- logIssueList('Overflowing Elements', result.overflowingElements, (el, i) => [
54
+ logIssueList('Overflowing Elements', details.overflowingElements, (el, i) => [
48
55
  `${i + 1}. <${el.tagName}> "${el.selector}"`,
49
56
  ` rect.right: ${el.rect.right}px (viewport: ${el.viewportWidth}px)`,
50
57
  ]);
51
- logIssueList('Clipped Text Elements', result.clippedTextElements, (el, i) => [
58
+ logIssueList('Clipped Text Elements', details.clippedTextElements, (el, i) => [
52
59
  `${i + 1}. <${el.tagName}> "${el.selector}"`,
53
60
  ` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
54
61
  ` overflow: ${el.overflow}, overflowX: ${el.overflowX}`,
@@ -15,13 +15,31 @@
15
15
  * - Cannot detect all redundant target cases
16
16
  * - CSS transform may affect measurements
17
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';
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, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
19
+ import { buildAuditResult, normalizeTargetSizeCheck, } from '../utils/axe-format.js';
19
20
  import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
20
21
  import { addPageAnnotations } from '../utils/annotations.js';
21
22
  /**
22
23
  * Collect basic target information from DOM (runs in browser context).
23
24
  */
24
- function collectBasicTargetInfo(interactiveSelector) {
25
+ function collectBasicTargetInfo(args) {
26
+ const { interactiveSelector, htmlSnippetMaxLength } = args;
27
+ function getHtmlSnippet(element) {
28
+ let html = '';
29
+ try {
30
+ html = element.outerHTML || '';
31
+ }
32
+ catch {
33
+ html = '';
34
+ }
35
+ if (!html) {
36
+ return { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
37
+ }
38
+ if (html.length > htmlSnippetMaxLength) {
39
+ return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
40
+ }
41
+ return { html, htmlTruncated: false };
42
+ }
25
43
  function getUniqueSelector(element, elementIndex) {
26
44
  if (element.id) {
27
45
  return `#${CSS.escape(element.id)}`;
@@ -72,6 +90,7 @@ function collectBasicTargetInfo(interactiveSelector) {
72
90
  targets.push({
73
91
  selector: getUniqueSelector(element, index),
74
92
  tagName,
93
+ ...getHtmlSnippet(element),
75
94
  role,
76
95
  width: Math.round(rect.width * 100) / 100,
77
96
  height: Math.round(rect.height * 100) / 100,
@@ -247,6 +266,8 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
247
266
  const issue = {
248
267
  selector: target.selector,
249
268
  tagName: target.tagName,
269
+ html: target.html,
270
+ htmlTruncated: target.htmlTruncated,
250
271
  role: target.role,
251
272
  accessibleName: target.accessibleName,
252
273
  width: target.width,
@@ -255,6 +276,9 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
255
276
  level,
256
277
  exception,
257
278
  exceptionDetails,
279
+ // the heuristics can detect exceptions but can never rule out the
280
+ // essential exception, so findings are at best 'possible'/'not-assessed'.
281
+ exceptionAssessment: exception ? 'possible' : 'not-assessed',
258
282
  href: target.href,
259
283
  };
260
284
  if (exception) {
@@ -276,7 +300,10 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
276
300
  export async function runTargetSizeCheck(options) {
277
301
  const { page, aaThreshold = TARGET_SIZE_AA, aaaThreshold = TARGET_SIZE_AAA, screenshot = false, ...location } = options;
278
302
  // Collect basic target info from DOM
279
- const basicTargets = await page.evaluate(collectBasicTargetInfo, INTERACTIVE_SELECTOR);
303
+ const basicTargets = await page.evaluate(collectBasicTargetInfo, {
304
+ interactiveSelector: INTERACTIVE_SELECTOR,
305
+ htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
306
+ });
280
307
  // Enhance with accessible names via ariaSnapshot()
281
308
  const targets = [];
282
309
  for (const basicTarget of basicTargets) {
@@ -296,8 +323,7 @@ export async function runTargetSizeCheck(options) {
296
323
  }
297
324
  // Analyze targets
298
325
  const { failAA, failAAAOnly, passCount, excepted } = analyzeTargets(targets, aaThreshold, aaaThreshold);
299
- const result = {
300
- url: page.url(),
326
+ const details = {
301
327
  totalTargetsChecked: targets.length,
302
328
  failAA,
303
329
  failAAAOnly,
@@ -310,16 +336,22 @@ export async function runTargetSizeCheck(options) {
310
336
  exceptedCount: excepted.length,
311
337
  },
312
338
  };
339
+ const result = buildAuditResult({
340
+ source: 'target-size-check',
341
+ url: page.url(),
342
+ details,
343
+ buckets: normalizeTargetSizeCheck(details),
344
+ });
313
345
  // Output results
314
346
  logAuditHeader('Target Size Check Results', 'WCAG 2.5.5 / 2.5.8', result.url);
315
347
  logSummary({
316
- 'Total targets checked': result.totalTargetsChecked,
348
+ 'Total targets checked': details.totalTargetsChecked,
317
349
  });
318
350
  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}`);
351
+ console.log(` Pass (>= ${aaaThreshold}px): ${details.summary.passCount}`);
352
+ console.log(` Fail AAA only (${aaThreshold}-${aaaThreshold - 1}px): ${details.summary.failAAAOnlyCount}`);
353
+ console.log(` Fail AA (< ${aaThreshold}px): ${details.summary.failAACount}`);
354
+ console.log(` Possible exceptions: ${details.summary.exceptedCount}`);
323
355
  logIssueList(`Fail AA (< ${aaThreshold}px) - Requires Fix`, failAA, (el, i) => {
324
356
  const lines = [
325
357
  `${i + 1}. <${el.tagName}> "${el.selector}"`,
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Text Spacing Check — WCAG 1.4.12 (Text Spacing)
3
+ *
4
+ * Captures baseline metrics, injects the WCAG 1.4.12 spacing overrides, and
5
+ * reports elements whose text becomes clipped (overflow:hidden) under the
6
+ * increased spacing.
7
+ *
8
+ * The caller is responsible for navigating the page before calling this.
9
+ */
10
+ import type { Page } from '@playwright/test';
11
+ import type { TextSpacingCheckResult } from '../types.js';
12
+ import { type OutputLocationOptions } from '../utils/test-harness.js';
13
+ export interface RunTextSpacingCheckOptions extends OutputLocationOptions {
14
+ /** A page already navigated to the target URL. */
15
+ page: Page;
16
+ /** Tolerance in pixels for clip detection (default: 2). */
17
+ tolerance?: number;
18
+ /** Whether to capture a screenshot next to the result file (default: false). */
19
+ screenshot?: boolean;
20
+ }
21
+ /**
22
+ * Run the text spacing check against the current page, write the result JSON
23
+ * (and optionally a screenshot), and return the parsed result.
24
+ */
25
+ export declare function runTextSpacingCheck(options: RunTextSpacingCheckOptions): Promise<TextSpacingCheckResult>;