@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,49 @@
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 * as fs from 'node:fs';
11
+ import { PNG } from 'pngjs';
12
+ import pixelmatch from 'pixelmatch';
13
+ import { PIXELMATCH_THRESHOLD } from '../constants.js';
14
+ /**
15
+ * Compare two PNG images using pixel-level diff.
16
+ *
17
+ * @returns Diff statistics
18
+ */
19
+ export function compareImages(img1Path, img2Path, diffOutputPath) {
20
+ const img1 = PNG.sync.read(fs.readFileSync(img1Path));
21
+ const img2 = PNG.sync.read(fs.readFileSync(img2Path));
22
+ const { width, height } = img1;
23
+ const totalPixels = width * height;
24
+ const diff = new PNG({ width, height });
25
+ const diffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {
26
+ threshold: PIXELMATCH_THRESHOLD,
27
+ });
28
+ fs.writeFileSync(diffOutputPath, PNG.sync.write(diff));
29
+ const diffPercent = (diffPixels / totalPixels) * 100;
30
+ return { diffPixels, totalPixels, diffPercent };
31
+ }
32
+ /** Format diff percentage as string with 3 decimal places */
33
+ export function formatDiffPercent(diffPercent) {
34
+ return diffPercent.toFixed(3) + '%';
35
+ }
36
+ /** Check if change is significant based on threshold */
37
+ export function hasSignificantChange(diffPercent, threshold) {
38
+ return diffPercent > threshold;
39
+ }
40
+ /** Ensure output directory exists */
41
+ export function ensureOutputDir(outputDir) {
42
+ if (!fs.existsSync(outputDir)) {
43
+ fs.mkdirSync(outputDir, { recursive: true });
44
+ }
45
+ }
46
+ /** Save JSON result to file */
47
+ export function saveJsonResult(filePath, data) {
48
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
49
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Recommendation generation for auto-play detection.
3
+ */
4
+ import type { PauseControlInfo, PauseVerificationResult } from '../types.js';
5
+ export interface RecommendationContext {
6
+ hasAutoPlayContent: boolean;
7
+ stopsWithin5Seconds: boolean;
8
+ pauseControls: PauseControlInfo;
9
+ pauseVerification: PauseVerificationResult;
10
+ }
11
+ /**
12
+ * Generate a recommendation based on detection results.
13
+ */
14
+ export declare function generateRecommendation(ctx: RecommendationContext): string;
15
+ /**
16
+ * Print summary to console.
17
+ */
18
+ export declare function printSummary(ctx: RecommendationContext, outputDir: string): void;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Recommendation generation for auto-play detection.
3
+ */
4
+ /**
5
+ * Generate a recommendation based on detection results.
6
+ */
7
+ export function generateRecommendation(ctx) {
8
+ const { hasAutoPlayContent, stopsWithin5Seconds, pauseControls, pauseVerification } = ctx;
9
+ if (!hasAutoPlayContent) {
10
+ return 'No auto-playing content detected in viewport.';
11
+ }
12
+ if (stopsWithin5Seconds) {
13
+ return 'Auto-playing content detected but stops within 5 seconds. WCAG 2.2.2 may be satisfied, but verify no user impact.';
14
+ }
15
+ if (pauseControls.found) {
16
+ if (pauseVerification.pauseWorked === true) {
17
+ return pauseControls.hasAccessibleName
18
+ ? 'Auto-playing content detected with working pause control. Verify keyboard accessibility.'
19
+ : 'Auto-playing content detected. Pause control works but lacks accessible name (aria-label). Add accessible labels (WCAG 4.1.2).';
20
+ }
21
+ if (pauseVerification.pauseWorked === false) {
22
+ return 'Auto-playing content detected. Pause control found but does NOT stop the animation. Fix the control or add a working one (WCAG 1.4.2, 2.2.2).';
23
+ }
24
+ // Verification not conclusive
25
+ return pauseControls.hasAccessibleName
26
+ ? 'Auto-playing content detected with pause controls. Manually verify controls work and are keyboard accessible.'
27
+ : 'Auto-playing content detected. Pause control found but lacks accessible name (aria-label). Add accessible labels (WCAG 1.4.2, 2.2.2, 4.1.2).';
28
+ }
29
+ return 'Auto-playing content detected and continues beyond 5 seconds. No pause/stop controls found. Add controls (WCAG 1.4.2, 2.2.2).';
30
+ }
31
+ /**
32
+ * Print summary to console.
33
+ */
34
+ export function printSummary(ctx, outputDir) {
35
+ const { hasAutoPlayContent, stopsWithin5Seconds, pauseControls, pauseVerification } = ctx;
36
+ console.log('\n--- Summary ---');
37
+ if (!hasAutoPlayContent) {
38
+ console.log('✓ No auto-playing content detected in viewport');
39
+ return;
40
+ }
41
+ console.log('⚠ Auto-playing content detected!');
42
+ console.log(` Stops within 5 seconds: ${stopsWithin5Seconds ? 'Yes' : 'No'}`);
43
+ console.log(` Screenshots saved to: ${outputDir}`);
44
+ console.log(' Diff images generated for visual verification');
45
+ // Pause control detection
46
+ console.log('\n--- Pause Control Detection ---');
47
+ if (pauseControls.found) {
48
+ console.log(`✓ Pause controls found: ${pauseControls.controls.length}`);
49
+ pauseControls.controls.forEach((ctrl, i) => {
50
+ console.log(` ${i + 1}. <${ctrl.element}> "${ctrl.name}" (matched by: ${ctrl.matchedBy})`);
51
+ });
52
+ if (!pauseControls.hasAccessibleName) {
53
+ console.log('⚠ Warning: Pause control lacks accessible name (aria-label)');
54
+ }
55
+ }
56
+ else {
57
+ console.log('✗ No pause controls detected');
58
+ }
59
+ // Pause verification results
60
+ if (pauseVerification.attempted) {
61
+ console.log('\n--- Pause Control Verification ---');
62
+ console.log(` Control clicked: ${pauseVerification.controlClicked}`);
63
+ console.log(` Change before click: ${pauseVerification.beforeClickDiffPercent}`);
64
+ console.log(` Change after click: ${pauseVerification.afterClickDiffPercent}`);
65
+ if (pauseVerification.pauseWorked === true) {
66
+ console.log('✓ Pause control WORKS - animation stopped after clicking');
67
+ }
68
+ else if (pauseVerification.pauseWorked === false) {
69
+ console.log('✗ Pause control DOES NOT WORK - animation continues after clicking');
70
+ }
71
+ else if (pauseVerification.error) {
72
+ console.log(`⚠ Verification error: ${pauseVerification.error}`);
73
+ }
74
+ }
75
+ if (pauseControls.carouselIndicators.length > 0) {
76
+ console.log(`\nCarousel navigation controls found: ${pauseControls.carouselIndicators.length}`);
77
+ }
78
+ // Manual verification checklist
79
+ console.log('\nManual verification required:');
80
+ console.log(' - Verify pause/stop controls are keyboard accessible');
81
+ console.log(' - Check for audio auto-play (requires manual listening)');
82
+ if (!pauseControls.hasAccessibleName && pauseControls.found) {
83
+ console.log(' - Add aria-label to pause control buttons');
84
+ }
85
+ if (pauseVerification.pauseWorked === false) {
86
+ console.log(' - Fix the pause control to actually stop the animation');
87
+ }
88
+ }
@@ -66,6 +66,12 @@ export declare function resolveScreenshotPath(resolvedResultPath: string, defaul
66
66
  * @throws if neither is provided.
67
67
  */
68
68
  export declare function requireTargetUrl(explicit?: string): string;
69
+ /**
70
+ * Resolve the target URL from the `TEST_PAGE` env var, falling back to the
71
+ * given default. Useful for callers that want a sensible default (e.g. a test
72
+ * fixture URL or preset) instead of throwing when `TEST_PAGE` is unset.
73
+ */
74
+ export declare function getTargetUrl(defaultPath: string): string;
69
75
  /** Log the header for audit results to console. */
70
76
  export declare function logAuditHeader(title: string, wcagRef: string, url: string): void;
71
77
  /** Log a summary section with key-value pairs. */
@@ -80,6 +80,14 @@ export function requireTargetUrl(explicit) {
80
80
  }
81
81
  return url;
82
82
  }
83
+ /**
84
+ * Resolve the target URL from the `TEST_PAGE` env var, falling back to the
85
+ * given default. Useful for callers that want a sensible default (e.g. a test
86
+ * fixture URL or preset) instead of throwing when `TEST_PAGE` is unset.
87
+ */
88
+ export function getTargetUrl(defaultPath) {
89
+ return process.env.TEST_PAGE || defaultPath;
90
+ }
83
91
  // =============================================================================
84
92
  // Console Logging
85
93
  // =============================================================================
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a11y-skills/audit",
3
- "version": "0.1.0",
4
- "description": "Playwright + axe-core based WCAG 2.2 accessibility audit functions (axe, focus indicator, reflow, target size).",
3
+ "version": "0.2.0",
4
+ "description": "Playwright + axe-core based WCAG 2.2 accessibility audit functions (axe, focus indicator, reflow, target size, text spacing, zoom, orientation, autocomplete, time limit, auto-play).",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -57,6 +57,30 @@
57
57
  "./test-entries/target-size-check": {
58
58
  "types": "./dist/test-entries/target-size-check.d.ts",
59
59
  "import": "./dist/test-entries/target-size-check.js"
60
+ },
61
+ "./test-entries/text-spacing-check": {
62
+ "types": "./dist/test-entries/text-spacing-check.d.ts",
63
+ "import": "./dist/test-entries/text-spacing-check.js"
64
+ },
65
+ "./test-entries/zoom-200-check": {
66
+ "types": "./dist/test-entries/zoom-200-check.d.ts",
67
+ "import": "./dist/test-entries/zoom-200-check.js"
68
+ },
69
+ "./test-entries/orientation-check": {
70
+ "types": "./dist/test-entries/orientation-check.d.ts",
71
+ "import": "./dist/test-entries/orientation-check.js"
72
+ },
73
+ "./test-entries/autocomplete-audit": {
74
+ "types": "./dist/test-entries/autocomplete-audit.d.ts",
75
+ "import": "./dist/test-entries/autocomplete-audit.js"
76
+ },
77
+ "./test-entries/time-limit-detector": {
78
+ "types": "./dist/test-entries/time-limit-detector.d.ts",
79
+ "import": "./dist/test-entries/time-limit-detector.js"
80
+ },
81
+ "./test-entries/auto-play-detection": {
82
+ "types": "./dist/test-entries/auto-play-detection.d.ts",
83
+ "import": "./dist/test-entries/auto-play-detection.js"
60
84
  }
61
85
  },
62
86
  "files": [
@@ -80,10 +104,15 @@
80
104
  "@axe-core/playwright": "^4.10.0",
81
105
  "@playwright/test": "^1.50.0"
82
106
  },
107
+ "optionalDependencies": {
108
+ "pixelmatch": "^7.1.0",
109
+ "pngjs": "^7.0.0"
110
+ },
83
111
  "devDependencies": {
84
112
  "@axe-core/playwright": "^4.10.0",
85
113
  "@playwright/test": "^1.50.0",
86
114
  "@types/node": "^22.10.0",
115
+ "@types/pngjs": "^6.0.5",
87
116
  "typescript": "^5.6.0"
88
117
  },
89
118
  "engines": {