@a11y-skills/audit 0.1.0 → 0.2.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 (45) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.ja.md +24 -2
  3. package/README.md +25 -2
  4. package/dist/constants.d.ts +82 -0
  5. package/dist/constants.js +223 -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/playwright/index.d.ts +7 -1
  11. package/dist/playwright/index.js +7 -1
  12. package/dist/playwright/runAutoPlayDetection.d.ts +36 -0
  13. package/dist/playwright/runAutoPlayDetection.js +137 -0
  14. package/dist/playwright/runAutocompleteAudit.d.ts +27 -0
  15. package/dist/playwright/runAutocompleteAudit.js +197 -0
  16. package/dist/playwright/runOrientationCheck.d.ts +40 -0
  17. package/dist/playwright/runOrientationCheck.js +164 -0
  18. package/dist/playwright/runTextSpacingCheck.d.ts +25 -0
  19. package/dist/playwright/runTextSpacingCheck.js +241 -0
  20. package/dist/playwright/runTimeLimitDetector.d.ts +31 -0
  21. package/dist/playwright/runTimeLimitDetector.js +194 -0
  22. package/dist/playwright/runZoomCheck.d.ts +42 -0
  23. package/dist/playwright/runZoomCheck.js +150 -0
  24. package/dist/schemas/index.d.ts +13 -1
  25. package/dist/schemas/index.js +122 -0
  26. package/dist/test-entries/auto-play-detection.d.ts +7 -0
  27. package/dist/test-entries/auto-play-detection.js +13 -0
  28. package/dist/test-entries/autocomplete-audit.d.ts +5 -0
  29. package/dist/test-entries/autocomplete-audit.js +11 -0
  30. package/dist/test-entries/orientation-check.d.ts +8 -0
  31. package/dist/test-entries/orientation-check.js +12 -0
  32. package/dist/test-entries/text-spacing-check.d.ts +5 -0
  33. package/dist/test-entries/text-spacing-check.js +11 -0
  34. package/dist/test-entries/time-limit-detector.d.ts +8 -0
  35. package/dist/test-entries/time-limit-detector.js +12 -0
  36. package/dist/test-entries/zoom-200-check.d.ts +5 -0
  37. package/dist/test-entries/zoom-200-check.js +11 -0
  38. package/dist/types.d.ts +151 -0
  39. package/dist/utils/image-compare.d.ts +24 -0
  40. package/dist/utils/image-compare.js +49 -0
  41. package/dist/utils/recommendations.d.ts +18 -0
  42. package/dist/utils/recommendations.js +88 -0
  43. package/dist/utils/test-harness.d.ts +6 -0
  44. package/dist/utils/test-harness.js +8 -0
  45. package/package.json +31 -2
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Zoom 200% Check — WCAG 1.4.4 (Resize Text)
3
+ *
4
+ * Sets a standard viewport (default 1280x720), applies 200% zoom via the CSS
5
+ * `zoom` property, then detects horizontal scrolling and elements whose content
6
+ * becomes clipped (overflow:hidden) under zoom.
7
+ *
8
+ * If a `targetUrl` (or the `TEST_PAGE` env var) is available, this function
9
+ * owns navigation: it sets the base viewport BEFORE navigating (matching the
10
+ * legacy script, so pages that read the viewport at load time behave the same).
11
+ * Otherwise it operates on the already-navigated page (just sets the viewport).
12
+ *
13
+ * Limitations:
14
+ * - CSS zoom is engine-specific; actual browser zoom may behave differently
15
+ * - Does not verify responsive breakpoint behavior
16
+ * - Manual verification needed for complex interactions at zoom
17
+ */
18
+ import type { Page } from '@playwright/test';
19
+ import type { ZoomCheckResult } from '../types.js';
20
+ import { type OutputLocationOptions } from '../utils/test-harness.js';
21
+ export interface RunZoomCheckOptions extends OutputLocationOptions {
22
+ /** Page to run the check on (navigated by this function if `targetUrl`/`TEST_PAGE` is set). */
23
+ page: Page;
24
+ /**
25
+ * Target URL. If provided (or `TEST_PAGE` is set), the function sets the base
26
+ * viewport then navigates, for results identical to the legacy script. If
27
+ * omitted, the page is assumed already navigated.
28
+ */
29
+ targetUrl?: string;
30
+ /** Base viewport applied before zoom (default: 1280x720). */
31
+ viewport?: {
32
+ width: number;
33
+ height: number;
34
+ };
35
+ /** Whether to capture a screenshot next to the result file (default: false). */
36
+ screenshot?: boolean;
37
+ }
38
+ /**
39
+ * Run the zoom 200% check against the current page, write the result JSON
40
+ * (and optionally a screenshot), and return the parsed result.
41
+ */
42
+ export declare function runZoomCheck(options: RunZoomCheckOptions): Promise<ZoomCheckResult>;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Zoom 200% Check — WCAG 1.4.4 (Resize Text)
3
+ *
4
+ * Sets a standard viewport (default 1280x720), applies 200% zoom via the CSS
5
+ * `zoom` property, then detects horizontal scrolling and elements whose content
6
+ * becomes clipped (overflow:hidden) under zoom.
7
+ *
8
+ * If a `targetUrl` (or the `TEST_PAGE` env var) is available, this function
9
+ * owns navigation: it sets the base viewport BEFORE navigating (matching the
10
+ * legacy script, so pages that read the viewport at load time behave the same).
11
+ * Otherwise it operates on the already-navigated page (just sets the viewport).
12
+ *
13
+ * Limitations:
14
+ * - CSS zoom is engine-specific; actual browser zoom may behave differently
15
+ * - Does not verify responsive breakpoint behavior
16
+ * - Manual verification needed for complex interactions at zoom
17
+ */
18
+ import { ZOOM_FACTOR, ZOOM_BASE_VIEWPORT, ZOOM_CLIP_TOLERANCE, REFLOW_CHECK_SELECTOR, DEFAULT_ZOOM_RESULT_FILE, DEFAULT_ZOOM_SCREENSHOT_FILE, } from '../constants.js';
19
+ import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
20
+ /** Apply zoom and detect issues in browser context. */
21
+ function applyZoomAndCheck(args) {
22
+ const { checkSelector, tolerance } = args;
23
+ // Apply CSS zoom
24
+ document.documentElement.style.zoom =
25
+ '200%';
26
+ // Force reflow
27
+ void document.body.offsetHeight;
28
+ function getUniqueSelector(element, elementIndex) {
29
+ if (element.id) {
30
+ return `#${element.id}`;
31
+ }
32
+ const path = [];
33
+ let current = element;
34
+ while (current && current !== document.body) {
35
+ let selector = current.tagName.toLowerCase();
36
+ const parent = current.parentElement;
37
+ if (parent) {
38
+ const childIndex = Array.from(parent.children).indexOf(current) + 1;
39
+ selector += `:nth-child(${childIndex})`;
40
+ }
41
+ path.unshift(selector);
42
+ current = parent;
43
+ }
44
+ return path.length > 0 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
45
+ }
46
+ function isVisible(element) {
47
+ const style = window.getComputedStyle(element);
48
+ return (style.display !== 'none' &&
49
+ style.visibility !== 'hidden' &&
50
+ parseFloat(style.opacity) > 0);
51
+ }
52
+ function hasHiddenOverflow(style) {
53
+ return (style.overflow === 'hidden' ||
54
+ style.overflow === 'clip' ||
55
+ style.overflowX === 'hidden' ||
56
+ style.overflowX === 'clip');
57
+ }
58
+ // Check document-level horizontal scroll
59
+ const scrollEl = document.scrollingElement || document.documentElement;
60
+ const documentScrollWidth = scrollEl.scrollWidth;
61
+ const documentClientWidth = scrollEl.clientWidth;
62
+ const hasHorizontalScroll = documentScrollWidth > documentClientWidth + tolerance;
63
+ // Find clipped elements
64
+ const clippedElements = [];
65
+ const seenElements = new WeakSet();
66
+ const elements = document.querySelectorAll(checkSelector);
67
+ elements.forEach((element, index) => {
68
+ if (!isVisible(element) || seenElements.has(element)) {
69
+ return;
70
+ }
71
+ const style = window.getComputedStyle(element);
72
+ if (!hasHiddenOverflow(style)) {
73
+ return;
74
+ }
75
+ const scrollWidth = element.scrollWidth;
76
+ const clientWidth = element.clientWidth;
77
+ const scrollHeight = element.scrollHeight;
78
+ const clientHeight = element.clientHeight;
79
+ const hasHorizontalClip = scrollWidth > clientWidth + tolerance;
80
+ const hasVerticalClip = scrollHeight > clientHeight + tolerance;
81
+ if ((hasHorizontalClip || hasVerticalClip) && element.textContent?.trim()) {
82
+ seenElements.add(element);
83
+ clippedElements.push({
84
+ selector: getUniqueSelector(element, index),
85
+ tagName: element.tagName.toLowerCase(),
86
+ scrollWidth,
87
+ clientWidth,
88
+ scrollHeight,
89
+ clientHeight,
90
+ issueType: hasHorizontalClip ? 'horizontal-scroll' : 'clipped-content',
91
+ });
92
+ }
93
+ });
94
+ return {
95
+ hasHorizontalScroll,
96
+ documentScrollWidth,
97
+ documentClientWidth,
98
+ clippedElements,
99
+ };
100
+ }
101
+ /**
102
+ * Run the zoom 200% check against the current page, write the result JSON
103
+ * (and optionally a screenshot), and return the parsed result.
104
+ */
105
+ export async function runZoomCheck(options) {
106
+ const { page, targetUrl: targetUrlOption, viewport = ZOOM_BASE_VIEWPORT, screenshot = false, ...location } = options;
107
+ await page.setViewportSize({ width: viewport.width, height: viewport.height });
108
+ // If a URL is available, navigate at the base viewport (legacy ordering).
109
+ const targetUrl = targetUrlOption ?? process.env.TEST_PAGE;
110
+ if (targetUrl) {
111
+ await page.goto(targetUrl, { waitUntil: 'networkidle' });
112
+ }
113
+ const zoomResult = await page.evaluate(applyZoomAndCheck, {
114
+ checkSelector: REFLOW_CHECK_SELECTOR,
115
+ tolerance: ZOOM_CLIP_TOLERANCE,
116
+ });
117
+ const result = {
118
+ url: page.url(),
119
+ zoomFactor: ZOOM_FACTOR,
120
+ viewport: { width: viewport.width, height: viewport.height },
121
+ ...zoomResult,
122
+ };
123
+ // Output results
124
+ logAuditHeader('Zoom 200% Check Results', 'WCAG 1.4.4', result.url);
125
+ logSummary({
126
+ 'Zoom factor': `${result.zoomFactor}x`,
127
+ 'Base viewport': `${result.viewport.width}x${result.viewport.height}`,
128
+ 'Document scroll width': `${result.documentScrollWidth}px`,
129
+ 'Document client width': `${result.documentClientWidth}px`,
130
+ 'Horizontal scroll': result.hasHorizontalScroll,
131
+ 'Clipped elements': result.clippedElements.length,
132
+ });
133
+ logIssueList('Clipped Elements', result.clippedElements, (el, i) => [
134
+ `${i + 1}. <${el.tagName}> "${el.selector}"`,
135
+ ` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
136
+ ` Issue: ${el.issueType}`,
137
+ ]);
138
+ const resolvedPath = saveAuditResult(result, {
139
+ ...location,
140
+ defaultFile: DEFAULT_ZOOM_RESULT_FILE,
141
+ });
142
+ let screenshotPath;
143
+ if (screenshot) {
144
+ screenshotPath = await takeAuditScreenshot(page, {
145
+ path: resolveScreenshotPath(resolvedPath, DEFAULT_ZOOM_SCREENSHOT_FILE),
146
+ });
147
+ }
148
+ logOutputPaths(resolvedPath, screenshotPath);
149
+ return result;
150
+ }
@@ -7,7 +7,7 @@
7
7
  * permissive (no `additionalProperties: false`) so that additive changes to a
8
8
  * result shape do not break downstream validation.
9
9
  */
10
- export type { AxeViolationNode, AxeViolation, AxeAuditResult, FocusRecord, OnFocusViolation, FocusCheckResult, BoundingRect, FocusObscuredOverlap, FocusObscuredIssue, ReflowIssue, ClippedTextElement, ReflowCheckResult, TargetSizeException, TargetSizeIssue, TargetSizeSummary, TargetSizeCheckResult, } from '../types.js';
10
+ export type { AxeViolationNode, AxeViolation, AxeAuditResult, FocusRecord, OnFocusViolation, FocusCheckResult, BoundingRect, FocusObscuredOverlap, FocusObscuredIssue, ReflowIssue, ClippedTextElement, ReflowCheckResult, TargetSizeException, TargetSizeIssue, TargetSizeSummary, TargetSizeCheckResult, TextSpacingIssue, TextSpacingCheckResult, ZoomIssue, ZoomCheckResult, OrientationState, OrientationCheckResult, AutocompleteIssue, AutocompleteAuditResult, MetaRefreshInfo, TimerInfo, CountdownIndicator, TimeLimitDetectorResult, ScreenshotRecord, ComparisonResult, ImageDiffResult, PauseControl, CarouselIndicator, PauseControlInfo, PauseVerificationResult, AutoPlayDetectionResult, } from '../types.js';
11
11
  /** Minimal JSON Schema object shape (Draft 2020-12 compatible subset). */
12
12
  export interface JsonSchema {
13
13
  $schema?: string;
@@ -25,10 +25,22 @@ export declare const AXE_AUDIT_RESULT_SCHEMA: JsonSchema;
25
25
  export declare const FOCUS_CHECK_RESULT_SCHEMA: JsonSchema;
26
26
  export declare const REFLOW_CHECK_RESULT_SCHEMA: JsonSchema;
27
27
  export declare const TARGET_SIZE_CHECK_RESULT_SCHEMA: JsonSchema;
28
+ export declare const TEXT_SPACING_CHECK_RESULT_SCHEMA: JsonSchema;
29
+ export declare const ZOOM_CHECK_RESULT_SCHEMA: JsonSchema;
30
+ export declare const ORIENTATION_CHECK_RESULT_SCHEMA: JsonSchema;
31
+ export declare const AUTOCOMPLETE_AUDIT_RESULT_SCHEMA: JsonSchema;
32
+ export declare const TIME_LIMIT_DETECTOR_RESULT_SCHEMA: JsonSchema;
33
+ export declare const AUTO_PLAY_DETECTION_RESULT_SCHEMA: JsonSchema;
28
34
  /** All result schemas keyed by check id. */
29
35
  export declare const RESULT_SCHEMAS: {
30
36
  readonly 'axe-audit': JsonSchema;
31
37
  readonly 'focus-indicator-check': JsonSchema;
32
38
  readonly 'reflow-check': JsonSchema;
33
39
  readonly 'target-size-check': JsonSchema;
40
+ readonly 'text-spacing-check': JsonSchema;
41
+ readonly 'zoom-200-check': JsonSchema;
42
+ readonly 'orientation-check': JsonSchema;
43
+ readonly 'autocomplete-audit': JsonSchema;
44
+ readonly 'time-limit-detector': JsonSchema;
45
+ readonly 'auto-play-detection': JsonSchema;
34
46
  };
@@ -236,10 +236,132 @@ export const TARGET_SIZE_CHECK_RESULT_SCHEMA = {
236
236
  disclaimer: DISCLAIMER_SCHEMA,
237
237
  },
238
238
  };
239
+ export const TEXT_SPACING_CHECK_RESULT_SCHEMA = {
240
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
241
+ $id: 'https://masup9.github.io/a11y-audit/schemas/text-spacing-check-result.json',
242
+ title: 'TextSpacingCheckResult',
243
+ type: 'object',
244
+ required: ['url', 'clippedElements', 'totalElementsChecked'],
245
+ properties: {
246
+ url: { type: 'string' },
247
+ clippedElements: { type: 'array', items: { type: 'object' } },
248
+ totalElementsChecked: { type: 'number' },
249
+ disclaimer: DISCLAIMER_SCHEMA,
250
+ },
251
+ };
252
+ export const ZOOM_CHECK_RESULT_SCHEMA = {
253
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
254
+ $id: 'https://masup9.github.io/a11y-audit/schemas/zoom-check-result.json',
255
+ title: 'ZoomCheckResult',
256
+ type: 'object',
257
+ required: [
258
+ 'url',
259
+ 'zoomFactor',
260
+ 'viewport',
261
+ 'hasHorizontalScroll',
262
+ 'documentScrollWidth',
263
+ 'documentClientWidth',
264
+ 'clippedElements',
265
+ ],
266
+ properties: {
267
+ url: { type: 'string' },
268
+ zoomFactor: { type: 'number' },
269
+ viewport: { type: 'object' },
270
+ hasHorizontalScroll: { type: 'boolean' },
271
+ documentScrollWidth: { type: 'number' },
272
+ documentClientWidth: { type: 'number' },
273
+ clippedElements: { type: 'array', items: { type: 'object' } },
274
+ disclaimer: DISCLAIMER_SCHEMA,
275
+ },
276
+ };
277
+ export const ORIENTATION_CHECK_RESULT_SCHEMA = {
278
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
279
+ $id: 'https://masup9.github.io/a11y-audit/schemas/orientation-check-result.json',
280
+ title: 'OrientationCheckResult',
281
+ type: 'object',
282
+ required: ['url', 'portrait', 'landscape', 'hasOrientationLock', 'lockDetectedIn'],
283
+ properties: {
284
+ url: { type: 'string' },
285
+ portrait: { type: 'object' },
286
+ landscape: { type: 'object' },
287
+ hasOrientationLock: { type: 'boolean' },
288
+ lockDetectedIn: {
289
+ type: 'string',
290
+ enum: ['portrait', 'landscape', 'both', 'none'],
291
+ },
292
+ disclaimer: DISCLAIMER_SCHEMA,
293
+ },
294
+ };
295
+ export const AUTOCOMPLETE_AUDIT_RESULT_SCHEMA = {
296
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
297
+ $id: 'https://masup9.github.io/a11y-audit/schemas/autocomplete-audit-result.json',
298
+ title: 'AutocompleteAuditResult',
299
+ type: 'object',
300
+ required: [
301
+ 'url',
302
+ 'totalFieldsChecked',
303
+ 'missingAutocomplete',
304
+ 'invalidAutocomplete',
305
+ ],
306
+ properties: {
307
+ url: { type: 'string' },
308
+ totalFieldsChecked: { type: 'number' },
309
+ missingAutocomplete: { type: 'array', items: { type: 'object' } },
310
+ invalidAutocomplete: { type: 'array', items: { type: 'object' } },
311
+ disclaimer: DISCLAIMER_SCHEMA,
312
+ },
313
+ };
314
+ export const TIME_LIMIT_DETECTOR_RESULT_SCHEMA = {
315
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
316
+ $id: 'https://masup9.github.io/a11y-audit/schemas/time-limit-detector-result.json',
317
+ title: 'TimeLimitDetectorResult',
318
+ type: 'object',
319
+ required: ['url', 'metaRefresh', 'timers', 'countdownIndicators', 'hasTimeLimits'],
320
+ properties: {
321
+ url: { type: 'string' },
322
+ metaRefresh: { type: 'array', items: { type: 'object' } },
323
+ timers: { type: 'array', items: { type: 'object' } },
324
+ countdownIndicators: { type: 'array', items: { type: 'object' } },
325
+ hasTimeLimits: { type: 'boolean' },
326
+ disclaimer: DISCLAIMER_SCHEMA,
327
+ },
328
+ };
329
+ export const AUTO_PLAY_DETECTION_RESULT_SCHEMA = {
330
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
331
+ $id: 'https://masup9.github.io/a11y-audit/schemas/auto-play-detection-result.json',
332
+ title: 'AutoPlayDetectionResult',
333
+ type: 'object',
334
+ required: [
335
+ 'url',
336
+ 'screenshotRecords',
337
+ 'comparisons',
338
+ 'hasAutoPlayContent',
339
+ 'stopsWithin5Seconds',
340
+ 'pauseControls',
341
+ 'pauseVerification',
342
+ 'recommendation',
343
+ ],
344
+ properties: {
345
+ url: { type: 'string' },
346
+ screenshotRecords: { type: 'array', items: { type: 'object' } },
347
+ comparisons: { type: 'array', items: { type: 'object' } },
348
+ hasAutoPlayContent: { type: 'boolean' },
349
+ stopsWithin5Seconds: { type: 'boolean' },
350
+ pauseControls: { type: 'object' },
351
+ pauseVerification: { type: 'object' },
352
+ recommendation: { type: 'string' },
353
+ },
354
+ };
239
355
  /** All result schemas keyed by check id. */
240
356
  export const RESULT_SCHEMAS = {
241
357
  'axe-audit': AXE_AUDIT_RESULT_SCHEMA,
242
358
  'focus-indicator-check': FOCUS_CHECK_RESULT_SCHEMA,
243
359
  'reflow-check': REFLOW_CHECK_RESULT_SCHEMA,
244
360
  'target-size-check': TARGET_SIZE_CHECK_RESULT_SCHEMA,
361
+ 'text-spacing-check': TEXT_SPACING_CHECK_RESULT_SCHEMA,
362
+ 'zoom-200-check': ZOOM_CHECK_RESULT_SCHEMA,
363
+ 'orientation-check': ORIENTATION_CHECK_RESULT_SCHEMA,
364
+ 'autocomplete-audit': AUTOCOMPLETE_AUDIT_RESULT_SCHEMA,
365
+ 'time-limit-detector': TIME_LIMIT_DETECTOR_RESULT_SCHEMA,
366
+ 'auto-play-detection': AUTO_PLAY_DETECTION_RESULT_SCHEMA,
245
367
  };
@@ -0,0 +1,7 @@
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
+ export {};
@@ -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
@@ -213,3 +213,154 @@ export interface TargetSizeCheckResult {
213
213
  /** Summary counts */
214
214
  summary: TargetSizeSummary;
215
215
  }
216
+ export interface TextSpacingIssue {
217
+ selector: string;
218
+ tagName: string;
219
+ beforeMetrics: {
220
+ scrollWidth: number;
221
+ scrollHeight: number;
222
+ clientWidth: number;
223
+ clientHeight: number;
224
+ };
225
+ afterMetrics: {
226
+ scrollWidth: number;
227
+ scrollHeight: number;
228
+ clientWidth: number;
229
+ clientHeight: number;
230
+ };
231
+ overflow: string;
232
+ overflowX: string;
233
+ overflowY: string;
234
+ issueType: 'horizontal-clip' | 'vertical-clip' | 'both';
235
+ }
236
+ export interface TextSpacingCheckResult {
237
+ url: string;
238
+ clippedElements: TextSpacingIssue[];
239
+ totalElementsChecked: number;
240
+ }
241
+ export interface ZoomIssue {
242
+ selector: string;
243
+ tagName: string;
244
+ scrollWidth: number;
245
+ clientWidth: number;
246
+ scrollHeight: number;
247
+ clientHeight: number;
248
+ issueType: 'horizontal-scroll' | 'clipped-content';
249
+ }
250
+ export interface ZoomCheckResult {
251
+ url: string;
252
+ zoomFactor: number;
253
+ viewport: {
254
+ width: number;
255
+ height: number;
256
+ };
257
+ hasHorizontalScroll: boolean;
258
+ documentScrollWidth: number;
259
+ documentClientWidth: number;
260
+ clippedElements: ZoomIssue[];
261
+ }
262
+ export interface OrientationState {
263
+ lockMessageFound: boolean;
264
+ lockMessageText: string | null;
265
+ mainContentHidden: boolean;
266
+ bodyWidth: number;
267
+ bodyHeight: number;
268
+ visibleTextLength: number;
269
+ }
270
+ export interface OrientationCheckResult {
271
+ url: string;
272
+ portrait: OrientationState;
273
+ landscape: OrientationState;
274
+ hasOrientationLock: boolean;
275
+ lockDetectedIn: 'portrait' | 'landscape' | 'both' | 'none';
276
+ }
277
+ export interface AutocompleteIssue {
278
+ selector: string;
279
+ tagName: string;
280
+ inputType: string;
281
+ name: string | null;
282
+ id: string | null;
283
+ labelText: string | null;
284
+ currentAutocomplete: string | null;
285
+ expectedToken: string;
286
+ matchedBy: 'name' | 'id' | 'label' | 'placeholder';
287
+ issueType: 'missing' | 'invalid';
288
+ }
289
+ export interface AutocompleteAuditResult {
290
+ url: string;
291
+ totalFieldsChecked: number;
292
+ missingAutocomplete: AutocompleteIssue[];
293
+ invalidAutocomplete: AutocompleteIssue[];
294
+ }
295
+ export interface MetaRefreshInfo {
296
+ content: string;
297
+ seconds: number;
298
+ url: string | null;
299
+ }
300
+ export interface TimerInfo {
301
+ type: 'setTimeout' | 'setInterval';
302
+ delayMs: number;
303
+ callStack: string | null;
304
+ }
305
+ export interface CountdownIndicator {
306
+ selector: string;
307
+ text: string;
308
+ tagName: string;
309
+ }
310
+ export interface TimeLimitDetectorResult {
311
+ url: string;
312
+ metaRefresh: MetaRefreshInfo[];
313
+ timers: TimerInfo[];
314
+ countdownIndicators: CountdownIndicator[];
315
+ hasTimeLimits: boolean;
316
+ }
317
+ export interface ScreenshotRecord {
318
+ time: string;
319
+ path: string;
320
+ }
321
+ export interface ComparisonResult {
322
+ compare: string;
323
+ diffPixels: number;
324
+ totalPixels: number;
325
+ diffPercent: string;
326
+ hasChange: boolean;
327
+ }
328
+ export interface ImageDiffResult {
329
+ diffPixels: number;
330
+ totalPixels: number;
331
+ diffPercent: number;
332
+ }
333
+ export interface PauseControl {
334
+ element: string;
335
+ name: string;
336
+ matchedBy: 'accessible-name' | 'class-name-near-carousel' | 'svg-icon-pattern';
337
+ selector: string;
338
+ }
339
+ export interface CarouselIndicator {
340
+ element: string;
341
+ name: string;
342
+ }
343
+ export interface PauseControlInfo {
344
+ found: boolean;
345
+ controls: PauseControl[];
346
+ carouselIndicators: CarouselIndicator[];
347
+ hasAccessibleName: boolean;
348
+ }
349
+ export interface PauseVerificationResult {
350
+ attempted: boolean;
351
+ controlClicked: string | null;
352
+ beforeClickDiffPercent: string | null;
353
+ afterClickDiffPercent: string | null;
354
+ pauseWorked: boolean | null;
355
+ error: string | null;
356
+ }
357
+ export interface AutoPlayDetectionResult {
358
+ url: string;
359
+ screenshotRecords: ScreenshotRecord[];
360
+ comparisons: ComparisonResult[];
361
+ hasAutoPlayContent: boolean;
362
+ stopsWithin5Seconds: boolean;
363
+ pauseControls: PauseControlInfo;
364
+ pauseVerification: PauseVerificationResult;
365
+ recommendation: string;
366
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Image comparison utilities using pixelmatch.
3
+ *
4
+ * NOTE: `pixelmatch` and `pngjs` are optional peer/runtime deps (only the
5
+ * auto-play check needs them). This module top-level imports them, so it must
6
+ * only be loaded via a dynamic `import()` from `runAutoPlayDetection` — never
7
+ * statically from the package barrel. That keeps the other checks usable when
8
+ * the optional deps are absent.
9
+ */
10
+ import type { ImageDiffResult } from '../types.js';
11
+ /**
12
+ * Compare two PNG images using pixel-level diff.
13
+ *
14
+ * @returns Diff statistics
15
+ */
16
+ export declare function compareImages(img1Path: string, img2Path: string, diffOutputPath: string): ImageDiffResult;
17
+ /** Format diff percentage as string with 3 decimal places */
18
+ export declare function formatDiffPercent(diffPercent: number): string;
19
+ /** Check if change is significant based on threshold */
20
+ export declare function hasSignificantChange(diffPercent: number, threshold: number): boolean;
21
+ /** Ensure output directory exists */
22
+ export declare function ensureOutputDir(outputDir: string): void;
23
+ /** Save JSON result to file */
24
+ export declare function saveJsonResult(filePath: string, data: unknown): void;