@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.
- package/CHANGELOG.md +34 -0
- package/README.ja.md +128 -0
- package/README.md +130 -0
- package/dist/constants.d.ts +88 -0
- package/dist/constants.js +184 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/playwright/index.d.ts +19 -0
- package/dist/playwright/index.js +19 -0
- package/dist/playwright/runAxeAudit.d.ts +28 -0
- package/dist/playwright/runAxeAudit.js +89 -0
- package/dist/playwright/runFocusIndicatorCheck.d.ts +30 -0
- package/dist/playwright/runFocusIndicatorCheck.js +740 -0
- package/dist/playwright/runReflowCheck.d.ts +36 -0
- package/dist/playwright/runReflowCheck.js +68 -0
- package/dist/playwright/runTargetSizeCheck.d.ts +35 -0
- package/dist/playwright/runTargetSizeCheck.js +389 -0
- package/dist/schemas/index.d.ts +34 -0
- package/dist/schemas/index.js +245 -0
- package/dist/test-entries/axe-audit.d.ts +13 -0
- package/dist/test-entries/axe-audit.js +20 -0
- package/dist/test-entries/focus-indicator-check.d.ts +9 -0
- package/dist/test-entries/focus-indicator-check.js +13 -0
- package/dist/test-entries/reflow-check.d.ts +9 -0
- package/dist/test-entries/reflow-check.js +21 -0
- package/dist/test-entries/target-size-check.d.ts +8 -0
- package/dist/test-entries/target-size-check.js +15 -0
- package/dist/types.d.ts +215 -0
- package/dist/types.js +4 -0
- package/dist/utils/annotations.d.ts +67 -0
- package/dist/utils/annotations.js +110 -0
- package/dist/utils/layout.d.ts +23 -0
- package/dist/utils/layout.js +144 -0
- package/dist/utils/test-harness.d.ts +76 -0
- package/dist/utils/test-harness.js +119 -0
- package/package.json +92 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotation overlay utilities for audit screenshots.
|
|
3
|
+
*
|
|
4
|
+
* Adds positioned boxes + labels over elements without modifying the content
|
|
5
|
+
* DOM. Used by the target size check to highlight pass/fail targets.
|
|
6
|
+
*/
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Annotation Styles
|
|
9
|
+
// =============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Standard annotation color schemes.
|
|
12
|
+
* Colors chosen for sufficient contrast with white text.
|
|
13
|
+
*/
|
|
14
|
+
export const ANNOTATION_COLORS = {
|
|
15
|
+
/** Pass - Dark green with white text */
|
|
16
|
+
pass: { bg: '#16a34a', border: '#16a34a', text: '#ffffff' },
|
|
17
|
+
/** Warning - Dark orange with white text */
|
|
18
|
+
warning: { bg: '#e65100', border: '#e65100', text: '#ffffff' },
|
|
19
|
+
/** Fail - Dark red with white text */
|
|
20
|
+
fail: { bg: '#dc2626', border: '#dc2626', text: '#ffffff' },
|
|
21
|
+
/** Info - Dark blue with white text */
|
|
22
|
+
info: { bg: '#0d47a1', border: '#0d47a1', text: '#ffffff' },
|
|
23
|
+
/** Violation - Purple with white text */
|
|
24
|
+
violation: { bg: '#7c3aed', border: '#7c3aed', text: '#ffffff' },
|
|
25
|
+
};
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Playwright Helper
|
|
28
|
+
// =============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Add annotations to a page using Playwright.
|
|
31
|
+
*
|
|
32
|
+
* Note: this mutates the page DOM (it appends an overlay element). Take any
|
|
33
|
+
* "clean" screenshot before calling this.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* await addPageAnnotations(page, [
|
|
37
|
+
* { selector: '#header', label: 'PASS', colorScheme: 'pass' },
|
|
38
|
+
* { selector: '.error', label: 'FAIL', colorScheme: 'fail' },
|
|
39
|
+
* ]);
|
|
40
|
+
*/
|
|
41
|
+
export async function addPageAnnotations(page, annotations) {
|
|
42
|
+
await page.evaluate((configs) => {
|
|
43
|
+
// Inline the colors to avoid serialization issues
|
|
44
|
+
const colors = {
|
|
45
|
+
pass: { bg: '#16a34a', border: '#16a34a', text: '#ffffff' },
|
|
46
|
+
warning: { bg: '#e65100', border: '#e65100', text: '#ffffff' },
|
|
47
|
+
fail: { bg: '#dc2626', border: '#dc2626', text: '#ffffff' },
|
|
48
|
+
info: { bg: '#0d47a1', border: '#0d47a1', text: '#ffffff' },
|
|
49
|
+
violation: { bg: '#7c3aed', border: '#7c3aed', text: '#ffffff' },
|
|
50
|
+
};
|
|
51
|
+
// Create overlay
|
|
52
|
+
let overlay = document.getElementById('wcag-audit-overlay');
|
|
53
|
+
if (!overlay) {
|
|
54
|
+
overlay = document.createElement('div');
|
|
55
|
+
overlay.id = 'wcag-audit-overlay';
|
|
56
|
+
overlay.style.cssText = `
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 0;
|
|
59
|
+
left: 0;
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
z-index: 99999;
|
|
64
|
+
`;
|
|
65
|
+
document.body.appendChild(overlay);
|
|
66
|
+
}
|
|
67
|
+
// Add annotations
|
|
68
|
+
for (const config of configs) {
|
|
69
|
+
try {
|
|
70
|
+
const element = document.querySelector(config.selector);
|
|
71
|
+
if (!element) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const rect = element.getBoundingClientRect();
|
|
75
|
+
const color = colors[config.colorScheme];
|
|
76
|
+
const box = document.createElement('div');
|
|
77
|
+
box.style.cssText = `
|
|
78
|
+
position: absolute;
|
|
79
|
+
left: ${rect.left + window.scrollX}px;
|
|
80
|
+
top: ${rect.top + window.scrollY}px;
|
|
81
|
+
width: ${rect.width}px;
|
|
82
|
+
height: ${rect.height}px;
|
|
83
|
+
border: 3px solid ${color.border};
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
`;
|
|
87
|
+
const labelEl = document.createElement('span');
|
|
88
|
+
labelEl.textContent = config.label;
|
|
89
|
+
labelEl.style.cssText = `
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: -22px;
|
|
92
|
+
left: -3px;
|
|
93
|
+
background: ${color.bg};
|
|
94
|
+
color: ${color.text};
|
|
95
|
+
font-size: 11px;
|
|
96
|
+
font-weight: bold;
|
|
97
|
+
padding: 2px 6px;
|
|
98
|
+
border-radius: 3px;
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
font-family: system-ui, sans-serif;
|
|
101
|
+
`;
|
|
102
|
+
box.appendChild(labelEl);
|
|
103
|
+
overlay.appendChild(box);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Ignore selector errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, annotations);
|
|
110
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout utilities for overflow and clipping detection.
|
|
3
|
+
* Used by the reflow check (and, in the future, zoom / text-spacing checks).
|
|
4
|
+
*/
|
|
5
|
+
import type { ReflowIssue, ClippedTextElement } from '../types.js';
|
|
6
|
+
export interface LayoutCheckOptions {
|
|
7
|
+
viewportWidth: number;
|
|
8
|
+
overflowTolerance: number;
|
|
9
|
+
checkSelector: string;
|
|
10
|
+
allowedOverflowSelectors: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export interface LayoutCheckResult {
|
|
13
|
+
hasHorizontalScroll: boolean;
|
|
14
|
+
documentScrollWidth: number;
|
|
15
|
+
documentClientWidth: number;
|
|
16
|
+
overflowingElements: ReflowIssue[];
|
|
17
|
+
clippedTextElements: ClippedTextElement[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create the browser-side layout check function.
|
|
21
|
+
* This is serialized and executed in the browser context.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createLayoutChecker(options: LayoutCheckOptions): LayoutCheckResult;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout utilities for overflow and clipping detection.
|
|
3
|
+
* Used by the reflow check (and, in the future, zoom / text-spacing checks).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create the browser-side layout check function.
|
|
7
|
+
* This is serialized and executed in the browser context.
|
|
8
|
+
*/
|
|
9
|
+
export function createLayoutChecker(options) {
|
|
10
|
+
const { viewportWidth, overflowTolerance, checkSelector, allowedOverflowSelectors, } = options;
|
|
11
|
+
// Helper functions (must be defined inside for browser context)
|
|
12
|
+
/**
|
|
13
|
+
* Generate a unique CSS selector for an element using index-based approach
|
|
14
|
+
* to avoid collisions with repeated components
|
|
15
|
+
*/
|
|
16
|
+
function getUniqueSelector(element, elementIndex) {
|
|
17
|
+
if (element.id) {
|
|
18
|
+
return `#${element.id}`;
|
|
19
|
+
}
|
|
20
|
+
const path = [];
|
|
21
|
+
let current = element;
|
|
22
|
+
while (current && current !== document.body) {
|
|
23
|
+
let selector = current.tagName.toLowerCase();
|
|
24
|
+
// Always use nth-child for uniqueness
|
|
25
|
+
const parent = current.parentElement;
|
|
26
|
+
if (parent) {
|
|
27
|
+
const childIndex = Array.from(parent.children).indexOf(current) + 1;
|
|
28
|
+
selector += `:nth-child(${childIndex})`;
|
|
29
|
+
}
|
|
30
|
+
path.unshift(selector);
|
|
31
|
+
current = parent;
|
|
32
|
+
}
|
|
33
|
+
// Append element index as fallback for guaranteed uniqueness
|
|
34
|
+
return path.length > 0 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
|
|
35
|
+
}
|
|
36
|
+
function isAllowedOverflow(element) {
|
|
37
|
+
return allowedOverflowSelectors.some((selector) => {
|
|
38
|
+
try {
|
|
39
|
+
return element.matches(selector) || element.closest(selector) !== null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
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
|
+
// Check document-level horizontal scroll
|
|
53
|
+
const scrollEl = document.scrollingElement || document.documentElement;
|
|
54
|
+
const documentScrollWidth = scrollEl.scrollWidth;
|
|
55
|
+
const documentClientWidth = scrollEl.clientWidth;
|
|
56
|
+
const hasHorizontalScroll = documentScrollWidth > documentClientWidth + overflowTolerance;
|
|
57
|
+
// Find overflowing elements
|
|
58
|
+
const overflowingElements = [];
|
|
59
|
+
const clippedTextElements = [];
|
|
60
|
+
// Use WeakSet to track elements by identity, not selector string
|
|
61
|
+
const seenElements = new WeakSet();
|
|
62
|
+
const elements = document.querySelectorAll(checkSelector);
|
|
63
|
+
elements.forEach((element, elementIndex) => {
|
|
64
|
+
if (!isVisible(element) || isAllowedOverflow(element)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Skip if we've already reported this element (by identity)
|
|
68
|
+
if (seenElements.has(element)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const rect = element.getBoundingClientRect();
|
|
72
|
+
const selector = getUniqueSelector(element, elementIndex);
|
|
73
|
+
// Check for right overflow (element extends beyond viewport)
|
|
74
|
+
if (rect.right > viewportWidth + overflowTolerance) {
|
|
75
|
+
seenElements.add(element);
|
|
76
|
+
overflowingElements.push({
|
|
77
|
+
selector,
|
|
78
|
+
tagName: element.tagName.toLowerCase(),
|
|
79
|
+
rect: {
|
|
80
|
+
left: Math.round(rect.left),
|
|
81
|
+
right: Math.round(rect.right),
|
|
82
|
+
width: Math.round(rect.width),
|
|
83
|
+
},
|
|
84
|
+
viewportWidth,
|
|
85
|
+
reason: 'overflow-right',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Check for left overflow (negative rect.left)
|
|
89
|
+
else if (rect.left < -overflowTolerance) {
|
|
90
|
+
seenElements.add(element);
|
|
91
|
+
overflowingElements.push({
|
|
92
|
+
selector,
|
|
93
|
+
tagName: element.tagName.toLowerCase(),
|
|
94
|
+
rect: {
|
|
95
|
+
left: Math.round(rect.left),
|
|
96
|
+
right: Math.round(rect.right),
|
|
97
|
+
width: Math.round(rect.width),
|
|
98
|
+
},
|
|
99
|
+
viewportWidth,
|
|
100
|
+
reason: 'overflow-left',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Check for clipped text (element has overflow hidden and content is clipped)
|
|
104
|
+
const style = window.getComputedStyle(element);
|
|
105
|
+
const overflow = style.overflow;
|
|
106
|
+
const overflowX = style.overflowX;
|
|
107
|
+
const isClipped = overflow === 'hidden' ||
|
|
108
|
+
overflow === 'clip' ||
|
|
109
|
+
overflowX === 'hidden' ||
|
|
110
|
+
overflowX === 'clip';
|
|
111
|
+
if (isClipped && !seenElements.has(element)) {
|
|
112
|
+
const scrollWidth = element.scrollWidth;
|
|
113
|
+
const clientWidth = element.clientWidth;
|
|
114
|
+
const scrollHeight = element.scrollHeight;
|
|
115
|
+
const clientHeight = element.clientHeight;
|
|
116
|
+
const hasHorizontalClip = scrollWidth > clientWidth + overflowTolerance;
|
|
117
|
+
const hasVerticalClip = scrollHeight > clientHeight + overflowTolerance;
|
|
118
|
+
if (hasHorizontalClip || hasVerticalClip) {
|
|
119
|
+
// Only report if element has text content
|
|
120
|
+
const hasText = element.textContent && element.textContent.trim().length > 0;
|
|
121
|
+
if (hasText) {
|
|
122
|
+
seenElements.add(element);
|
|
123
|
+
clippedTextElements.push({
|
|
124
|
+
selector,
|
|
125
|
+
tagName: element.tagName.toLowerCase(),
|
|
126
|
+
scrollWidth,
|
|
127
|
+
clientWidth,
|
|
128
|
+
scrollHeight,
|
|
129
|
+
clientHeight,
|
|
130
|
+
overflow,
|
|
131
|
+
overflowX,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
hasHorizontalScroll,
|
|
139
|
+
documentScrollWidth,
|
|
140
|
+
documentClientWidth,
|
|
141
|
+
overflowingElements,
|
|
142
|
+
clippedTextElements,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test harness utilities for the audit checks.
|
|
3
|
+
*
|
|
4
|
+
* Provides output-path resolution (options → env → cwd), result writing, and
|
|
5
|
+
* console logging helpers shared by the four checks.
|
|
6
|
+
*/
|
|
7
|
+
import type { Page } from '@playwright/test';
|
|
8
|
+
/**
|
|
9
|
+
* Shared options for controlling where a check writes its result file.
|
|
10
|
+
*
|
|
11
|
+
* Resolution order:
|
|
12
|
+
* 1. `outputPath` (absolute or relative full path) — mutually exclusive with
|
|
13
|
+
* `outputDir` / `outputFile`.
|
|
14
|
+
* 2. `outputDir` option → `A11Y_OUTPUT_DIR` env → `process.cwd()`, joined with
|
|
15
|
+
* `outputFile` option → the check's default filename.
|
|
16
|
+
*/
|
|
17
|
+
export interface OutputLocationOptions {
|
|
18
|
+
/** Directory to write result/screenshot into. Falls back to `A11Y_OUTPUT_DIR`, then cwd. */
|
|
19
|
+
outputDir?: string;
|
|
20
|
+
/** Result file name (basename only). Falls back to the check's default. */
|
|
21
|
+
outputFile?: string;
|
|
22
|
+
/** Full path for the result file. Mutually exclusive with `outputDir`/`outputFile`. */
|
|
23
|
+
outputPath?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the absolute path a check should write its result to.
|
|
27
|
+
*
|
|
28
|
+
* @throws if `outputPath` is combined with `outputDir`/`outputFile`, or if
|
|
29
|
+
* `outputFile` is an absolute path.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveOutputPath(options: OutputLocationOptions & {
|
|
32
|
+
defaultFile: string;
|
|
33
|
+
}): string;
|
|
34
|
+
export interface SaveResultOptions extends OutputLocationOptions {
|
|
35
|
+
/** Default file name when none is provided via options. */
|
|
36
|
+
defaultFile: string;
|
|
37
|
+
/** Whether to append the disclaimer to the written JSON (default: true). */
|
|
38
|
+
includeDisclaimer?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Save an audit result to a JSON file, creating parent directories as needed.
|
|
42
|
+
*
|
|
43
|
+
* @returns the absolute path the result was written to.
|
|
44
|
+
*/
|
|
45
|
+
export declare function saveAuditResult<T extends object>(result: T, options: SaveResultOptions): string;
|
|
46
|
+
export interface TakeScreenshotOptions {
|
|
47
|
+
/** Absolute or relative path to write the screenshot to. */
|
|
48
|
+
path: string;
|
|
49
|
+
/** Whether to capture the full page (default: true). */
|
|
50
|
+
fullPage?: boolean;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Take a screenshot with standard audit settings, creating parent dirs.
|
|
54
|
+
*
|
|
55
|
+
* @returns the absolute path the screenshot was written to.
|
|
56
|
+
*/
|
|
57
|
+
export declare function takeAuditScreenshot(page: Page, options: TakeScreenshotOptions): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the screenshot path so it sits next to the resolved result file,
|
|
60
|
+
* using the given default screenshot filename when no explicit path is set.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveScreenshotPath(resolvedResultPath: string, defaultScreenshotFile: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the target URL from an explicit value or the `TEST_PAGE` env var.
|
|
65
|
+
*
|
|
66
|
+
* @throws if neither is provided.
|
|
67
|
+
*/
|
|
68
|
+
export declare function requireTargetUrl(explicit?: string): string;
|
|
69
|
+
/** Log the header for audit results to console. */
|
|
70
|
+
export declare function logAuditHeader(title: string, wcagRef: string, url: string): void;
|
|
71
|
+
/** Log a summary section with key-value pairs. */
|
|
72
|
+
export declare function logSummary(items: Record<string, string | number | boolean>): void;
|
|
73
|
+
/** Log a list of issues with truncation. */
|
|
74
|
+
export declare function logIssueList<T>(title: string, items: T[], formatter: (item: T, index: number) => string[], maxItems?: number): void;
|
|
75
|
+
/** Log the output file paths and disclaimer. */
|
|
76
|
+
export declare function logOutputPaths(outputPath: string, screenshotPath?: string): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test harness utilities for the audit checks.
|
|
3
|
+
*
|
|
4
|
+
* Provides output-path resolution (options → env → cwd), result writing, and
|
|
5
|
+
* console logging helpers shared by the four checks.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { AUDIT_DISCLAIMER, DISCLAIMER_CONSOLE } from '../constants.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the absolute path a check should write its result to.
|
|
12
|
+
*
|
|
13
|
+
* @throws if `outputPath` is combined with `outputDir`/`outputFile`, or if
|
|
14
|
+
* `outputFile` is an absolute path.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveOutputPath(options) {
|
|
17
|
+
const { outputDir, outputFile, outputPath, defaultFile } = options;
|
|
18
|
+
if (outputPath !== undefined &&
|
|
19
|
+
(outputDir !== undefined || outputFile !== undefined)) {
|
|
20
|
+
throw new Error('`outputPath` cannot be combined with `outputDir` / `outputFile`. ' +
|
|
21
|
+
'Specify either `outputPath`, or `outputDir` + `outputFile`.');
|
|
22
|
+
}
|
|
23
|
+
if (outputPath !== undefined) {
|
|
24
|
+
return path.resolve(outputPath);
|
|
25
|
+
}
|
|
26
|
+
if (outputFile !== undefined && path.basename(outputFile) !== outputFile) {
|
|
27
|
+
throw new Error('`outputFile` must be a bare file name (no path separators or `..`). ' +
|
|
28
|
+
'Use `outputDir` for the directory, or `outputPath` for a full path.');
|
|
29
|
+
}
|
|
30
|
+
const dir = outputDir ?? process.env.A11Y_OUTPUT_DIR ?? process.cwd();
|
|
31
|
+
const file = outputFile ?? defaultFile;
|
|
32
|
+
return path.resolve(dir, file);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Save an audit result to a JSON file, creating parent directories as needed.
|
|
36
|
+
*
|
|
37
|
+
* @returns the absolute path the result was written to.
|
|
38
|
+
*/
|
|
39
|
+
export function saveAuditResult(result, options) {
|
|
40
|
+
const { defaultFile, includeDisclaimer = true, ...location } = options;
|
|
41
|
+
const resolvedPath = resolveOutputPath({ ...location, defaultFile });
|
|
42
|
+
const outputData = includeDisclaimer
|
|
43
|
+
? { ...result, disclaimer: AUDIT_DISCLAIMER }
|
|
44
|
+
: result;
|
|
45
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
46
|
+
fs.writeFileSync(resolvedPath, JSON.stringify(outputData, null, 2));
|
|
47
|
+
return resolvedPath;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Take a screenshot with standard audit settings, creating parent dirs.
|
|
51
|
+
*
|
|
52
|
+
* @returns the absolute path the screenshot was written to.
|
|
53
|
+
*/
|
|
54
|
+
export async function takeAuditScreenshot(page, options) {
|
|
55
|
+
const { path: screenshotPath, fullPage = true } = options;
|
|
56
|
+
const resolvedPath = path.resolve(screenshotPath);
|
|
57
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
58
|
+
await page.screenshot({ path: resolvedPath, fullPage });
|
|
59
|
+
return resolvedPath;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the screenshot path so it sits next to the resolved result file,
|
|
63
|
+
* using the given default screenshot filename when no explicit path is set.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveScreenshotPath(resolvedResultPath, defaultScreenshotFile) {
|
|
66
|
+
return path.join(path.dirname(resolvedResultPath), defaultScreenshotFile);
|
|
67
|
+
}
|
|
68
|
+
// =============================================================================
|
|
69
|
+
// URL resolution (used by compatibility test-entries)
|
|
70
|
+
// =============================================================================
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the target URL from an explicit value or the `TEST_PAGE` env var.
|
|
73
|
+
*
|
|
74
|
+
* @throws if neither is provided.
|
|
75
|
+
*/
|
|
76
|
+
export function requireTargetUrl(explicit) {
|
|
77
|
+
const url = explicit ?? process.env.TEST_PAGE;
|
|
78
|
+
if (!url) {
|
|
79
|
+
throw new Error('No target URL provided. Pass `targetUrl` or set the TEST_PAGE environment variable.');
|
|
80
|
+
}
|
|
81
|
+
return url;
|
|
82
|
+
}
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Console Logging
|
|
85
|
+
// =============================================================================
|
|
86
|
+
/** Log the header for audit results to console. */
|
|
87
|
+
export function logAuditHeader(title, wcagRef, url) {
|
|
88
|
+
console.log(`\n=== ${title} (${wcagRef}) ===`);
|
|
89
|
+
console.log(`URL: ${url}`);
|
|
90
|
+
}
|
|
91
|
+
/** Log a summary section with key-value pairs. */
|
|
92
|
+
export function logSummary(items) {
|
|
93
|
+
for (const [label, value] of Object.entries(items)) {
|
|
94
|
+
const displayValue = typeof value === 'boolean' ? (value ? 'YES' : 'No') : value;
|
|
95
|
+
console.log(`${label}: ${displayValue}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Log a list of issues with truncation. */
|
|
99
|
+
export function logIssueList(title, items, formatter, maxItems = 10) {
|
|
100
|
+
if (items.length === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(`\n--- ${title} ---`);
|
|
104
|
+
items.slice(0, maxItems).forEach((item, index) => {
|
|
105
|
+
const lines = formatter(item, index);
|
|
106
|
+
lines.forEach((line) => console.log(` ${line}`));
|
|
107
|
+
});
|
|
108
|
+
if (items.length > maxItems) {
|
|
109
|
+
console.log(` ... and ${items.length - maxItems} more`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Log the output file paths and disclaimer. */
|
|
113
|
+
export function logOutputPaths(outputPath, screenshotPath) {
|
|
114
|
+
console.log(`\nResults saved to: ${outputPath}`);
|
|
115
|
+
if (screenshotPath) {
|
|
116
|
+
console.log(`Screenshot saved to: ${screenshotPath}`);
|
|
117
|
+
}
|
|
118
|
+
console.log(DISCLAIMER_CONSOLE);
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@masup9/a11y-audit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Playwright + axe-core based WCAG 2.2 accessibility audit functions (axe, focus indicator, reflow, target size).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "masuP9",
|
|
9
|
+
"url": "https://github.com/masuP9"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/masuP9/a11y-specialist-skills",
|
|
14
|
+
"directory": "packages/a11y-audit"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/masuP9/a11y-specialist-skills/tree/main/packages/a11y-audit",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/masuP9/a11y-specialist-skills/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"accessibility",
|
|
22
|
+
"a11y",
|
|
23
|
+
"wcag",
|
|
24
|
+
"wcag22",
|
|
25
|
+
"playwright",
|
|
26
|
+
"axe-core",
|
|
27
|
+
"audit",
|
|
28
|
+
"focus-indicator",
|
|
29
|
+
"reflow",
|
|
30
|
+
"target-size"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./playwright": {
|
|
38
|
+
"types": "./dist/playwright/index.d.ts",
|
|
39
|
+
"import": "./dist/playwright/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./schemas": {
|
|
42
|
+
"types": "./dist/schemas/index.d.ts",
|
|
43
|
+
"import": "./dist/schemas/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./test-entries/axe-audit": {
|
|
46
|
+
"types": "./dist/test-entries/axe-audit.d.ts",
|
|
47
|
+
"import": "./dist/test-entries/axe-audit.js"
|
|
48
|
+
},
|
|
49
|
+
"./test-entries/focus-indicator-check": {
|
|
50
|
+
"types": "./dist/test-entries/focus-indicator-check.d.ts",
|
|
51
|
+
"import": "./dist/test-entries/focus-indicator-check.js"
|
|
52
|
+
},
|
|
53
|
+
"./test-entries/reflow-check": {
|
|
54
|
+
"types": "./dist/test-entries/reflow-check.d.ts",
|
|
55
|
+
"import": "./dist/test-entries/reflow-check.js"
|
|
56
|
+
},
|
|
57
|
+
"./test-entries/target-size-check": {
|
|
58
|
+
"types": "./dist/test-entries/target-size-check.d.ts",
|
|
59
|
+
"import": "./dist/test-entries/target-size-check.js"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"dist",
|
|
64
|
+
"README.md",
|
|
65
|
+
"README.ja.md",
|
|
66
|
+
"CHANGELOG.md"
|
|
67
|
+
],
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public"
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"clean": "rm -rf dist",
|
|
73
|
+
"build": "tsc -p tsconfig.json",
|
|
74
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
75
|
+
"pretest": "npm run build",
|
|
76
|
+
"test": "playwright test",
|
|
77
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"@axe-core/playwright": "^4.10.0",
|
|
81
|
+
"@playwright/test": "^1.50.0"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@axe-core/playwright": "^4.10.0",
|
|
85
|
+
"@playwright/test": "^1.50.0",
|
|
86
|
+
"@types/node": "^22.10.0",
|
|
87
|
+
"typescript": "^5.6.0"
|
|
88
|
+
},
|
|
89
|
+
"engines": {
|
|
90
|
+
"node": ">=18"
|
|
91
|
+
}
|
|
92
|
+
}
|