@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.
- package/CHANGELOG.md +65 -0
- package/README.ja.md +76 -6
- package/README.md +78 -6
- package/dist/constants.d.ts +84 -0
- package/dist/constants.js +228 -0
- package/dist/detectors/index.d.ts +1 -0
- package/dist/detectors/index.js +1 -0
- package/dist/detectors/pause-control.d.ts +18 -0
- package/dist/detectors/pause-control.js +206 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/playwright/index.d.ts +7 -1
- package/dist/playwright/index.js +7 -1
- package/dist/playwright/runAutoPlayDetection.d.ts +36 -0
- package/dist/playwright/runAutoPlayDetection.js +143 -0
- package/dist/playwright/runAutocompleteAudit.d.ts +27 -0
- package/dist/playwright/runAutocompleteAudit.js +227 -0
- package/dist/playwright/runAxeAudit.d.ts +4 -0
- package/dist/playwright/runAxeAudit.js +26 -30
- package/dist/playwright/runFocusIndicatorCheck.js +55 -12
- package/dist/playwright/runOrientationCheck.d.ts +40 -0
- package/dist/playwright/runOrientationCheck.js +170 -0
- package/dist/playwright/runReflowCheck.js +18 -11
- package/dist/playwright/runTargetSizeCheck.js +42 -10
- package/dist/playwright/runTextSpacingCheck.d.ts +25 -0
- package/dist/playwright/runTextSpacingCheck.js +285 -0
- package/dist/playwright/runTimeLimitDetector.d.ts +31 -0
- package/dist/playwright/runTimeLimitDetector.js +219 -0
- package/dist/playwright/runZoomCheck.d.ts +42 -0
- package/dist/playwright/runZoomCheck.js +174 -0
- package/dist/schemas/index.d.ts +20 -1
- package/dist/schemas/index.js +404 -186
- package/dist/test-entries/auto-play-detection.d.ts +7 -0
- package/dist/test-entries/auto-play-detection.js +13 -0
- package/dist/test-entries/autocomplete-audit.d.ts +5 -0
- package/dist/test-entries/autocomplete-audit.js +11 -0
- package/dist/test-entries/orientation-check.d.ts +8 -0
- package/dist/test-entries/orientation-check.js +12 -0
- package/dist/test-entries/text-spacing-check.d.ts +5 -0
- package/dist/test-entries/text-spacing-check.js +11 -0
- package/dist/test-entries/time-limit-detector.d.ts +8 -0
- package/dist/test-entries/time-limit-detector.js +12 -0
- package/dist/test-entries/zoom-200-check.d.ts +5 -0
- package/dist/test-entries/zoom-200-check.js +11 -0
- package/dist/types.d.ts +275 -40
- package/dist/types.js +9 -0
- package/dist/utils/axe-format.d.ts +88 -0
- package/dist/utils/axe-format.js +361 -0
- package/dist/utils/image-compare.d.ts +24 -0
- package/dist/utils/image-compare.js +49 -0
- package/dist/utils/layout.d.ts +2 -0
- package/dist/utils/layout.js +20 -1
- package/dist/utils/recommendations.d.ts +18 -0
- package/dist/utils/recommendations.js +88 -0
- package/dist/utils/rule-registry.d.ts +216 -0
- package/dist/utils/rule-registry.js +220 -0
- package/dist/utils/test-harness.d.ts +8 -2
- package/dist/utils/test-harness.js +13 -6
- package/package.json +32 -2
|
@@ -0,0 +1,174 @@
|
|
|
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, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
19
|
+
import { buildAuditResult, normalizeZoomCheck } from '../utils/axe-format.js';
|
|
20
|
+
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
21
|
+
/** Apply zoom and detect issues in browser context. */
|
|
22
|
+
function applyZoomAndCheck(args) {
|
|
23
|
+
const { checkSelector, tolerance, htmlSnippetMaxLength } = args;
|
|
24
|
+
function getHtmlSnippet(element) {
|
|
25
|
+
let html = '';
|
|
26
|
+
try {
|
|
27
|
+
html = element.outerHTML || '';
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
html = '';
|
|
31
|
+
}
|
|
32
|
+
if (!html) {
|
|
33
|
+
return { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
|
|
34
|
+
}
|
|
35
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
36
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
37
|
+
}
|
|
38
|
+
return { html, htmlTruncated: false };
|
|
39
|
+
}
|
|
40
|
+
// Apply CSS zoom
|
|
41
|
+
document.documentElement.style.zoom =
|
|
42
|
+
'200%';
|
|
43
|
+
// Force reflow
|
|
44
|
+
void document.body.offsetHeight;
|
|
45
|
+
function getUniqueSelector(element, elementIndex) {
|
|
46
|
+
if (element.id) {
|
|
47
|
+
return `#${element.id}`;
|
|
48
|
+
}
|
|
49
|
+
const path = [];
|
|
50
|
+
let current = element;
|
|
51
|
+
while (current && current !== document.body) {
|
|
52
|
+
let selector = current.tagName.toLowerCase();
|
|
53
|
+
const parent = current.parentElement;
|
|
54
|
+
if (parent) {
|
|
55
|
+
const childIndex = Array.from(parent.children).indexOf(current) + 1;
|
|
56
|
+
selector += `:nth-child(${childIndex})`;
|
|
57
|
+
}
|
|
58
|
+
path.unshift(selector);
|
|
59
|
+
current = parent;
|
|
60
|
+
}
|
|
61
|
+
return path.length > 0 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
|
|
62
|
+
}
|
|
63
|
+
function isVisible(element) {
|
|
64
|
+
const style = window.getComputedStyle(element);
|
|
65
|
+
return (style.display !== 'none' &&
|
|
66
|
+
style.visibility !== 'hidden' &&
|
|
67
|
+
parseFloat(style.opacity) > 0);
|
|
68
|
+
}
|
|
69
|
+
function hasHiddenOverflow(style) {
|
|
70
|
+
return (style.overflow === 'hidden' ||
|
|
71
|
+
style.overflow === 'clip' ||
|
|
72
|
+
style.overflowX === 'hidden' ||
|
|
73
|
+
style.overflowX === 'clip');
|
|
74
|
+
}
|
|
75
|
+
// Check document-level horizontal scroll
|
|
76
|
+
const scrollEl = document.scrollingElement || document.documentElement;
|
|
77
|
+
const documentScrollWidth = scrollEl.scrollWidth;
|
|
78
|
+
const documentClientWidth = scrollEl.clientWidth;
|
|
79
|
+
const hasHorizontalScroll = documentScrollWidth > documentClientWidth + tolerance;
|
|
80
|
+
// Find clipped elements
|
|
81
|
+
const clippedElements = [];
|
|
82
|
+
const seenElements = new WeakSet();
|
|
83
|
+
const elements = document.querySelectorAll(checkSelector);
|
|
84
|
+
elements.forEach((element, index) => {
|
|
85
|
+
if (!isVisible(element) || seenElements.has(element)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const style = window.getComputedStyle(element);
|
|
89
|
+
if (!hasHiddenOverflow(style)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const scrollWidth = element.scrollWidth;
|
|
93
|
+
const clientWidth = element.clientWidth;
|
|
94
|
+
const scrollHeight = element.scrollHeight;
|
|
95
|
+
const clientHeight = element.clientHeight;
|
|
96
|
+
const hasHorizontalClip = scrollWidth > clientWidth + tolerance;
|
|
97
|
+
const hasVerticalClip = scrollHeight > clientHeight + tolerance;
|
|
98
|
+
if ((hasHorizontalClip || hasVerticalClip) && element.textContent?.trim()) {
|
|
99
|
+
seenElements.add(element);
|
|
100
|
+
clippedElements.push({
|
|
101
|
+
selector: getUniqueSelector(element, index),
|
|
102
|
+
tagName: element.tagName.toLowerCase(),
|
|
103
|
+
...getHtmlSnippet(element),
|
|
104
|
+
scrollWidth,
|
|
105
|
+
clientWidth,
|
|
106
|
+
scrollHeight,
|
|
107
|
+
clientHeight,
|
|
108
|
+
issueType: hasHorizontalClip ? 'horizontal-scroll' : 'clipped-content',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
hasHorizontalScroll,
|
|
114
|
+
documentScrollWidth,
|
|
115
|
+
documentClientWidth,
|
|
116
|
+
clippedElements,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Run the zoom 200% check against the current page, write the result JSON
|
|
121
|
+
* (and optionally a screenshot), and return the parsed result.
|
|
122
|
+
*/
|
|
123
|
+
export async function runZoomCheck(options) {
|
|
124
|
+
const { page, targetUrl: targetUrlOption, viewport = ZOOM_BASE_VIEWPORT, screenshot = false, ...location } = options;
|
|
125
|
+
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
126
|
+
// If a URL is available, navigate at the base viewport (legacy ordering).
|
|
127
|
+
const targetUrl = targetUrlOption ?? process.env.TEST_PAGE;
|
|
128
|
+
if (targetUrl) {
|
|
129
|
+
await page.goto(targetUrl, { waitUntil: 'networkidle' });
|
|
130
|
+
}
|
|
131
|
+
const zoomResult = await page.evaluate(applyZoomAndCheck, {
|
|
132
|
+
checkSelector: REFLOW_CHECK_SELECTOR,
|
|
133
|
+
tolerance: ZOOM_CLIP_TOLERANCE,
|
|
134
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
135
|
+
});
|
|
136
|
+
const details = {
|
|
137
|
+
zoomFactor: ZOOM_FACTOR,
|
|
138
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
139
|
+
...zoomResult,
|
|
140
|
+
};
|
|
141
|
+
const result = buildAuditResult({
|
|
142
|
+
source: 'zoom-200-check',
|
|
143
|
+
url: page.url(),
|
|
144
|
+
details,
|
|
145
|
+
buckets: normalizeZoomCheck(details),
|
|
146
|
+
});
|
|
147
|
+
// Output results
|
|
148
|
+
logAuditHeader('Zoom 200% Check Results', 'WCAG 1.4.4', result.url);
|
|
149
|
+
logSummary({
|
|
150
|
+
'Zoom factor': `${details.zoomFactor}x`,
|
|
151
|
+
'Base viewport': `${details.viewport.width}x${details.viewport.height}`,
|
|
152
|
+
'Document scroll width': `${details.documentScrollWidth}px`,
|
|
153
|
+
'Document client width': `${details.documentClientWidth}px`,
|
|
154
|
+
'Horizontal scroll': details.hasHorizontalScroll,
|
|
155
|
+
'Clipped elements': details.clippedElements.length,
|
|
156
|
+
});
|
|
157
|
+
logIssueList('Clipped Elements', details.clippedElements, (el, i) => [
|
|
158
|
+
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
159
|
+
` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
|
|
160
|
+
` Issue: ${el.issueType}`,
|
|
161
|
+
]);
|
|
162
|
+
const resolvedPath = saveAuditResult(result, {
|
|
163
|
+
...location,
|
|
164
|
+
defaultFile: DEFAULT_ZOOM_RESULT_FILE,
|
|
165
|
+
});
|
|
166
|
+
let screenshotPath;
|
|
167
|
+
if (screenshot) {
|
|
168
|
+
screenshotPath = await takeAuditScreenshot(page, {
|
|
169
|
+
path: resolveScreenshotPath(resolvedPath, DEFAULT_ZOOM_SCREENSHOT_FILE),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
logOutputPaths(resolvedPath, screenshotPath);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -6,18 +6,25 @@
|
|
|
6
6
|
* (e.g. an issue creator that reads `*-result.json`). They are intentionally
|
|
7
7
|
* permissive (no `additionalProperties: false`) so that additive changes to a
|
|
8
8
|
* result shape do not break downstream validation.
|
|
9
|
+
*
|
|
10
|
+
* Every check shares the same envelope (`source` / `url` / `timestamp` / the
|
|
11
|
+
* four normalized buckets / `summary` / `details` / `disclaimer`); the common
|
|
12
|
+
* pieces live in `$defs` and each check schema defines its own `details`.
|
|
9
13
|
*/
|
|
10
|
-
export type {
|
|
14
|
+
export type { AuditCheckResult, AuditResultSummary, CheckSource, NormalizedImpact, NormalizedNode, NormalizedRuleResult, AxeAuditDetails, AxeAuditResult, FocusRecord, FocusElementRef, OnFocusViolation, FocusCheckDetails, FocusCheckResult, BoundingRect, FocusObscuredOverlap, FocusObscuredIssue, ReflowIssue, ClippedTextElement, ReflowCheckDetails, ReflowCheckResult, TargetSizeException, TargetSizeExceptionAssessment, TargetSizeIssue, TargetSizeSummary, TargetSizeCheckDetails, TargetSizeCheckResult, TextSpacingIssue, TextSpacingCheckDetails, TextSpacingCheckResult, ZoomIssue, ZoomCheckDetails, ZoomCheckResult, OrientationState, OrientationCheckDetails, OrientationCheckResult, AutocompleteIssue, AutocompleteAuditDetails, AutocompleteAuditResult, MetaRefreshInfo, TimerInfo, CountdownIndicator, TimeLimitDetectorDetails, TimeLimitDetectorResult, ScreenshotRecord, ComparisonResult, ImageDiffResult, PauseControl, CarouselIndicator, PauseControlInfo, PauseVerificationResult, AutoPlayDetectionDetails, AutoPlayDetectionResult, } from '../types.js';
|
|
11
15
|
/** Minimal JSON Schema object shape (Draft 2020-12 compatible subset). */
|
|
12
16
|
export interface JsonSchema {
|
|
13
17
|
$schema?: string;
|
|
14
18
|
$id?: string;
|
|
19
|
+
$ref?: string;
|
|
20
|
+
$defs?: Record<string, JsonSchema>;
|
|
15
21
|
title?: string;
|
|
16
22
|
type?: string | string[];
|
|
17
23
|
properties?: Record<string, JsonSchema>;
|
|
18
24
|
items?: JsonSchema;
|
|
19
25
|
required?: string[];
|
|
20
26
|
enum?: unknown[];
|
|
27
|
+
const?: unknown;
|
|
21
28
|
description?: string;
|
|
22
29
|
[key: string]: unknown;
|
|
23
30
|
}
|
|
@@ -25,10 +32,22 @@ export declare const AXE_AUDIT_RESULT_SCHEMA: JsonSchema;
|
|
|
25
32
|
export declare const FOCUS_CHECK_RESULT_SCHEMA: JsonSchema;
|
|
26
33
|
export declare const REFLOW_CHECK_RESULT_SCHEMA: JsonSchema;
|
|
27
34
|
export declare const TARGET_SIZE_CHECK_RESULT_SCHEMA: JsonSchema;
|
|
35
|
+
export declare const TEXT_SPACING_CHECK_RESULT_SCHEMA: JsonSchema;
|
|
36
|
+
export declare const ZOOM_CHECK_RESULT_SCHEMA: JsonSchema;
|
|
37
|
+
export declare const ORIENTATION_CHECK_RESULT_SCHEMA: JsonSchema;
|
|
38
|
+
export declare const AUTOCOMPLETE_AUDIT_RESULT_SCHEMA: JsonSchema;
|
|
39
|
+
export declare const TIME_LIMIT_DETECTOR_RESULT_SCHEMA: JsonSchema;
|
|
40
|
+
export declare const AUTO_PLAY_DETECTION_RESULT_SCHEMA: JsonSchema;
|
|
28
41
|
/** All result schemas keyed by check id. */
|
|
29
42
|
export declare const RESULT_SCHEMAS: {
|
|
30
43
|
readonly 'axe-audit': JsonSchema;
|
|
31
44
|
readonly 'focus-indicator-check': JsonSchema;
|
|
32
45
|
readonly 'reflow-check': JsonSchema;
|
|
33
46
|
readonly 'target-size-check': JsonSchema;
|
|
47
|
+
readonly 'text-spacing-check': JsonSchema;
|
|
48
|
+
readonly 'zoom-200-check': JsonSchema;
|
|
49
|
+
readonly 'orientation-check': JsonSchema;
|
|
50
|
+
readonly 'autocomplete-audit': JsonSchema;
|
|
51
|
+
readonly 'time-limit-detector': JsonSchema;
|
|
52
|
+
readonly 'auto-play-detection': JsonSchema;
|
|
34
53
|
};
|