@a11y-skills/audit 0.2.0 → 0.3.1
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 +54 -0
- package/README.ja.md +52 -4
- package/README.md +53 -4
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +104 -21
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.js +1 -1
- package/dist/playwright/runAutoPlayDetection.js +15 -4
- package/dist/playwright/runAutocompleteAudit.js +46 -11
- package/dist/playwright/runAxeAudit.d.ts +4 -0
- package/dist/playwright/runAxeAudit.js +26 -30
- package/dist/playwright/runFocusIndicatorCheck.d.ts +7 -0
- package/dist/playwright/runFocusIndicatorCheck.js +98 -31
- package/dist/playwright/runOrientationCheck.js +13 -7
- package/dist/playwright/runReflowCheck.js +22 -12
- package/dist/playwright/runTargetSizeCheck.js +49 -12
- package/dist/playwright/runTextSpacingCheck.js +64 -10
- package/dist/playwright/runTimeLimitDetector.js +56 -25
- package/dist/playwright/runZoomCheck.js +46 -15
- package/dist/schemas/index.d.ts +8 -1
- package/dist/schemas/index.js +397 -292
- package/dist/types.d.ts +137 -53
- package/dist/types.js +9 -0
- package/dist/utils/axe-format.d.ts +88 -0
- package/dist/utils/axe-format.js +370 -0
- package/dist/utils/layout.d.ts +2 -0
- package/dist/utils/layout.js +26 -2
- package/dist/utils/recommendations.js +2 -2
- package/dist/utils/rule-registry.d.ts +216 -0
- package/dist/utils/rule-registry.js +220 -0
- package/dist/utils/test-harness.d.ts +2 -2
- package/dist/utils/test-harness.js +5 -6
- package/package.json +2 -1
|
@@ -7,11 +7,31 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The caller is responsible for navigating the page before calling this.
|
|
9
9
|
*/
|
|
10
|
-
import { TEXT_SPACING_CSS, TEXT_SPACING_CLIP_TOLERANCE, TEXT_SPACING_CHECK_SELECTOR, DEFAULT_TEXT_SPACING_RESULT_FILE, DEFAULT_TEXT_SPACING_SCREENSHOT_FILE, } from '../constants.js';
|
|
10
|
+
import { TEXT_SPACING_CSS, TEXT_SPACING_CLIP_TOLERANCE, TEXT_SPACING_CHECK_SELECTOR, DEFAULT_TEXT_SPACING_RESULT_FILE, DEFAULT_TEXT_SPACING_SCREENSHOT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
11
|
+
import { buildAuditResult, normalizeTextSpacingCheck, } from '../utils/axe-format.js';
|
|
11
12
|
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
12
13
|
/** Collect metrics for elements with hidden overflow (browser context). */
|
|
13
14
|
function collectElementMetrics(args) {
|
|
14
|
-
const { checkSelector } = args;
|
|
15
|
+
const { checkSelector, htmlSnippetMaxLength } = args;
|
|
16
|
+
function getHtmlSnippet(element) {
|
|
17
|
+
let html = '';
|
|
18
|
+
try {
|
|
19
|
+
html = element.outerHTML || '';
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
html = '';
|
|
23
|
+
}
|
|
24
|
+
if (!html) {
|
|
25
|
+
return {
|
|
26
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
27
|
+
htmlTruncated: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
31
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
32
|
+
}
|
|
33
|
+
return { html, htmlTruncated: false };
|
|
34
|
+
}
|
|
15
35
|
function getUniqueSelector(element, elementIndex) {
|
|
16
36
|
if (element.id) {
|
|
17
37
|
return `#${element.id}`;
|
|
@@ -28,7 +48,9 @@ function collectElementMetrics(args) {
|
|
|
28
48
|
path.unshift(selector);
|
|
29
49
|
current = parent;
|
|
30
50
|
}
|
|
31
|
-
return path.length > 0
|
|
51
|
+
return path.length > 0
|
|
52
|
+
? path.join(' > ')
|
|
53
|
+
: `[data-index="${elementIndex}"]`;
|
|
32
54
|
}
|
|
33
55
|
function isVisible(element) {
|
|
34
56
|
const style = window.getComputedStyle(element);
|
|
@@ -56,6 +78,7 @@ function collectElementMetrics(args) {
|
|
|
56
78
|
metrics.push({
|
|
57
79
|
selector: getUniqueSelector(element, index),
|
|
58
80
|
tagName: element.tagName.toLowerCase(),
|
|
81
|
+
...getHtmlSnippet(element),
|
|
59
82
|
scrollWidth: element.scrollWidth,
|
|
60
83
|
scrollHeight: element.scrollHeight,
|
|
61
84
|
clientWidth: element.clientWidth,
|
|
@@ -70,13 +93,32 @@ function collectElementMetrics(args) {
|
|
|
70
93
|
}
|
|
71
94
|
/** Inject text spacing CSS and re-collect metrics (browser context). */
|
|
72
95
|
function injectSpacingAndCollect(args) {
|
|
73
|
-
const { css, checkSelector } = args;
|
|
96
|
+
const { css, checkSelector, htmlSnippetMaxLength } = args;
|
|
74
97
|
const styleEl = document.createElement('style');
|
|
75
98
|
styleEl.id = 'wcag-text-spacing-override';
|
|
76
99
|
styleEl.textContent = css;
|
|
77
100
|
document.head.appendChild(styleEl);
|
|
78
101
|
// Force reflow
|
|
79
102
|
void document.body.offsetHeight;
|
|
103
|
+
function getHtmlSnippet(element) {
|
|
104
|
+
let html = '';
|
|
105
|
+
try {
|
|
106
|
+
html = element.outerHTML || '';
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
html = '';
|
|
110
|
+
}
|
|
111
|
+
if (!html) {
|
|
112
|
+
return {
|
|
113
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
114
|
+
htmlTruncated: false,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
118
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
119
|
+
}
|
|
120
|
+
return { html, htmlTruncated: false };
|
|
121
|
+
}
|
|
80
122
|
function getUniqueSelector(element, elementIndex) {
|
|
81
123
|
if (element.id) {
|
|
82
124
|
return `#${element.id}`;
|
|
@@ -93,7 +135,9 @@ function injectSpacingAndCollect(args) {
|
|
|
93
135
|
path.unshift(selector);
|
|
94
136
|
current = parent;
|
|
95
137
|
}
|
|
96
|
-
return path.length > 0
|
|
138
|
+
return path.length > 0
|
|
139
|
+
? path.join(' > ')
|
|
140
|
+
: `[data-index="${elementIndex}"]`;
|
|
97
141
|
}
|
|
98
142
|
function isVisible(element) {
|
|
99
143
|
const style = window.getComputedStyle(element);
|
|
@@ -121,6 +165,7 @@ function injectSpacingAndCollect(args) {
|
|
|
121
165
|
metrics.push({
|
|
122
166
|
selector: getUniqueSelector(element, index),
|
|
123
167
|
tagName: element.tagName.toLowerCase(),
|
|
168
|
+
...getHtmlSnippet(element),
|
|
124
169
|
scrollWidth: element.scrollWidth,
|
|
125
170
|
scrollHeight: element.scrollHeight,
|
|
126
171
|
clientWidth: element.clientWidth,
|
|
@@ -175,6 +220,8 @@ function detectClippingIssues(beforeMetrics, afterMetrics, tolerance) {
|
|
|
175
220
|
issues.push({
|
|
176
221
|
selector: after.selector,
|
|
177
222
|
tagName: after.tagName,
|
|
223
|
+
html: after.html,
|
|
224
|
+
htmlTruncated: after.htmlTruncated,
|
|
178
225
|
beforeMetrics: {
|
|
179
226
|
scrollWidth: beforeData.scrollWidth,
|
|
180
227
|
scrollHeight: beforeData.scrollHeight,
|
|
@@ -204,23 +251,30 @@ export async function runTextSpacingCheck(options) {
|
|
|
204
251
|
const { page, tolerance = TEXT_SPACING_CLIP_TOLERANCE, screenshot = false, ...location } = options;
|
|
205
252
|
const beforeMetrics = await page.evaluate(collectElementMetrics, {
|
|
206
253
|
checkSelector: TEXT_SPACING_CHECK_SELECTOR,
|
|
254
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
207
255
|
});
|
|
208
256
|
const afterMetrics = await page.evaluate(injectSpacingAndCollect, {
|
|
209
257
|
css: TEXT_SPACING_CSS,
|
|
210
258
|
checkSelector: TEXT_SPACING_CHECK_SELECTOR,
|
|
259
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
211
260
|
});
|
|
212
261
|
const clippedElements = detectClippingIssues(beforeMetrics, afterMetrics, tolerance);
|
|
213
|
-
const
|
|
214
|
-
url: page.url(),
|
|
262
|
+
const details = {
|
|
215
263
|
clippedElements,
|
|
216
264
|
totalElementsChecked: afterMetrics.length,
|
|
217
265
|
};
|
|
266
|
+
const result = buildAuditResult({
|
|
267
|
+
source: 'text-spacing-check',
|
|
268
|
+
url: page.url(),
|
|
269
|
+
details,
|
|
270
|
+
buckets: normalizeTextSpacingCheck(details),
|
|
271
|
+
});
|
|
218
272
|
logAuditHeader('Text Spacing Check Results', 'WCAG 1.4.12', result.url);
|
|
219
273
|
logSummary({
|
|
220
|
-
'Elements with overflow:hidden checked':
|
|
221
|
-
'Elements with clipping issues':
|
|
274
|
+
'Elements with overflow:hidden checked': details.totalElementsChecked,
|
|
275
|
+
'Elements with clipping issues': details.clippedElements.length,
|
|
222
276
|
});
|
|
223
|
-
logIssueList('Clipped Elements',
|
|
277
|
+
logIssueList('Clipped Elements', details.clippedElements, (el, i) => [
|
|
224
278
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
225
279
|
` Issue: ${el.issueType}`,
|
|
226
280
|
` Before: ${el.beforeMetrics.scrollWidth}x${el.beforeMetrics.scrollHeight} in ${el.beforeMetrics.clientWidth}x${el.beforeMetrics.clientHeight}`,
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* (option → `TEST_PAGE` env → required). Unlike the focus check it does not
|
|
11
11
|
* need a fresh context per attempt, so it stays page-based.
|
|
12
12
|
*/
|
|
13
|
-
import { TIME_LIMIT_KEYWORDS, TIME_LIMIT_THRESHOLD_MS, TIME_LIMIT_MIN_MS, DEFAULT_TIME_LIMIT_RESULT_FILE, } from '../constants.js';
|
|
13
|
+
import { TIME_LIMIT_KEYWORDS, TIME_LIMIT_THRESHOLD_MS, TIME_LIMIT_MIN_MS, DEFAULT_TIME_LIMIT_RESULT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
14
|
+
import { buildAuditResult, normalizeTimeLimitDetector, } from '../utils/axe-format.js';
|
|
14
15
|
import { saveAuditResult, requireTargetUrl, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
15
16
|
/** Timer hook injected before page load (browser context). */
|
|
16
17
|
function createTimerHookScript(args) {
|
|
@@ -29,13 +30,13 @@ function createTimerHookScript(args) {
|
|
|
29
30
|
callStack =
|
|
30
31
|
e.stack?.split('\n').slice(2, 5).join('\n') || null;
|
|
31
32
|
}
|
|
32
|
-
capturedTimers.push({
|
|
33
|
+
capturedTimers.push({
|
|
34
|
+
type: 'setTimeout',
|
|
35
|
+
delayMs: actualDelay,
|
|
36
|
+
callStack,
|
|
37
|
+
});
|
|
33
38
|
}
|
|
34
|
-
return originalSetTimeout.apply(window, [
|
|
35
|
-
callback,
|
|
36
|
-
delay,
|
|
37
|
-
...rest,
|
|
38
|
-
]);
|
|
39
|
+
return originalSetTimeout.apply(window, [callback, delay, ...rest]);
|
|
39
40
|
};
|
|
40
41
|
window.setInterval = function (callback, delay, ...rest) {
|
|
41
42
|
const actualDelay = delay || 0;
|
|
@@ -48,19 +49,39 @@ function createTimerHookScript(args) {
|
|
|
48
49
|
callStack =
|
|
49
50
|
e.stack?.split('\n').slice(2, 5).join('\n') || null;
|
|
50
51
|
}
|
|
51
|
-
capturedTimers.push({
|
|
52
|
+
capturedTimers.push({
|
|
53
|
+
type: 'setInterval',
|
|
54
|
+
delayMs: actualDelay,
|
|
55
|
+
callStack,
|
|
56
|
+
});
|
|
52
57
|
}
|
|
53
|
-
return originalSetInterval.apply(window, [
|
|
54
|
-
callback,
|
|
55
|
-
delay,
|
|
56
|
-
...rest,
|
|
57
|
-
]);
|
|
58
|
+
return originalSetInterval.apply(window, [callback, delay, ...rest]);
|
|
58
59
|
};
|
|
59
|
-
window.__capturedTimers =
|
|
60
|
+
window.__capturedTimers =
|
|
61
|
+
capturedTimers;
|
|
60
62
|
}
|
|
61
63
|
/** Detect meta refresh + countdown indicators (browser context). */
|
|
62
64
|
function detectTimeLimitIndicators(args) {
|
|
63
|
-
const { keywords } = args;
|
|
65
|
+
const { keywords, htmlSnippetMaxLength } = args;
|
|
66
|
+
function getHtmlSnippet(element) {
|
|
67
|
+
let html = '';
|
|
68
|
+
try {
|
|
69
|
+
html = element.outerHTML || '';
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
html = '';
|
|
73
|
+
}
|
|
74
|
+
if (!html) {
|
|
75
|
+
return {
|
|
76
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
77
|
+
htmlTruncated: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
81
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
82
|
+
}
|
|
83
|
+
return { html, htmlTruncated: false };
|
|
84
|
+
}
|
|
64
85
|
function getUniqueSelector(element, elementIndex) {
|
|
65
86
|
if (element.id) {
|
|
66
87
|
return `#${element.id}`;
|
|
@@ -77,7 +98,9 @@ function detectTimeLimitIndicators(args) {
|
|
|
77
98
|
path.unshift(selector);
|
|
78
99
|
current = parent;
|
|
79
100
|
}
|
|
80
|
-
return path.length > 0
|
|
101
|
+
return path.length > 0
|
|
102
|
+
? path.join(' > ')
|
|
103
|
+
: `[data-index="${elementIndex}"]`;
|
|
81
104
|
}
|
|
82
105
|
const metaRefresh = [];
|
|
83
106
|
const metaTags = document.querySelectorAll('meta[http-equiv="refresh"]');
|
|
@@ -91,6 +114,7 @@ function detectTimeLimitIndicators(args) {
|
|
|
91
114
|
content,
|
|
92
115
|
seconds: parseInt(match[1] ?? '0', 10),
|
|
93
116
|
url: match[2]?.trim() || null,
|
|
117
|
+
...getHtmlSnippet(meta),
|
|
94
118
|
});
|
|
95
119
|
}
|
|
96
120
|
}
|
|
@@ -116,6 +140,7 @@ function detectTimeLimitIndicators(args) {
|
|
|
116
140
|
selector: getUniqueSelector(parent, elementIndex),
|
|
117
141
|
text: fullText,
|
|
118
142
|
tagName: parent.tagName.toLowerCase(),
|
|
143
|
+
...getHtmlSnippet(parent),
|
|
119
144
|
});
|
|
120
145
|
}
|
|
121
146
|
}
|
|
@@ -139,25 +164,31 @@ export async function runTimeLimitDetector(options) {
|
|
|
139
164
|
});
|
|
140
165
|
const indicators = await page.evaluate(detectTimeLimitIndicators, {
|
|
141
166
|
keywords: [...TIME_LIMIT_KEYWORDS],
|
|
167
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
142
168
|
});
|
|
143
169
|
const hasTimeLimits = indicators.metaRefresh.length > 0 ||
|
|
144
170
|
timers.length > 0 ||
|
|
145
171
|
indicators.countdownIndicators.length > 0;
|
|
146
|
-
const
|
|
147
|
-
url: page.url(),
|
|
172
|
+
const details = {
|
|
148
173
|
metaRefresh: indicators.metaRefresh,
|
|
149
174
|
timers,
|
|
150
175
|
countdownIndicators: indicators.countdownIndicators,
|
|
151
176
|
hasTimeLimits,
|
|
152
177
|
};
|
|
178
|
+
const result = buildAuditResult({
|
|
179
|
+
source: 'time-limit-detector',
|
|
180
|
+
url: page.url(),
|
|
181
|
+
details,
|
|
182
|
+
buckets: normalizeTimeLimitDetector(details),
|
|
183
|
+
});
|
|
153
184
|
logAuditHeader('Time Limit Detection Results', 'WCAG 2.2.1', result.url);
|
|
154
185
|
logSummary({
|
|
155
|
-
'Meta refresh tags':
|
|
156
|
-
[`Timers detected (${minMs / 1000}s - ${maxMs / 1000}s)`]:
|
|
157
|
-
'Countdown text indicators':
|
|
158
|
-
'Time limits detected':
|
|
186
|
+
'Meta refresh tags': details.metaRefresh.length,
|
|
187
|
+
[`Timers detected (${minMs / 1000}s - ${maxMs / 1000}s)`]: details.timers.length,
|
|
188
|
+
'Countdown text indicators': details.countdownIndicators.length,
|
|
189
|
+
'Time limits detected': details.hasTimeLimits,
|
|
159
190
|
});
|
|
160
|
-
logIssueList('Meta Refresh',
|
|
191
|
+
logIssueList('Meta Refresh', details.metaRefresh, (meta, i) => {
|
|
161
192
|
const lines = [
|
|
162
193
|
`${i + 1}. content="${meta.content}"`,
|
|
163
194
|
` Refresh in ${meta.seconds} seconds`,
|
|
@@ -167,7 +198,7 @@ export async function runTimeLimitDetector(options) {
|
|
|
167
198
|
}
|
|
168
199
|
return lines;
|
|
169
200
|
});
|
|
170
|
-
logIssueList('Detected Timers',
|
|
201
|
+
logIssueList('Detected Timers', details.timers, (timer, i) => {
|
|
171
202
|
const lines = [
|
|
172
203
|
`${i + 1}. ${timer.type} - ${timer.delayMs}ms (${(timer.delayMs / 1000).toFixed(1)}s)`,
|
|
173
204
|
];
|
|
@@ -176,7 +207,7 @@ export async function runTimeLimitDetector(options) {
|
|
|
176
207
|
}
|
|
177
208
|
return lines;
|
|
178
209
|
});
|
|
179
|
-
logIssueList('Countdown Indicators',
|
|
210
|
+
logIssueList('Countdown Indicators', details.countdownIndicators, (indicator, i) => {
|
|
180
211
|
const truncatedText = indicator.text.length > 80
|
|
181
212
|
? indicator.text.slice(0, 80) + '...'
|
|
182
213
|
: indicator.text;
|
|
@@ -15,14 +15,33 @@
|
|
|
15
15
|
* - Does not verify responsive breakpoint behavior
|
|
16
16
|
* - Manual verification needed for complex interactions at zoom
|
|
17
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';
|
|
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';
|
|
19
20
|
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
20
21
|
/** Apply zoom and detect issues in browser context. */
|
|
21
22
|
function applyZoomAndCheck(args) {
|
|
22
|
-
const { checkSelector, tolerance } = 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 {
|
|
34
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
35
|
+
htmlTruncated: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
39
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
40
|
+
}
|
|
41
|
+
return { html, htmlTruncated: false };
|
|
42
|
+
}
|
|
23
43
|
// Apply CSS zoom
|
|
24
|
-
document.documentElement.style.zoom =
|
|
25
|
-
'200%';
|
|
44
|
+
document.documentElement.style.zoom = '200%';
|
|
26
45
|
// Force reflow
|
|
27
46
|
void document.body.offsetHeight;
|
|
28
47
|
function getUniqueSelector(element, elementIndex) {
|
|
@@ -41,7 +60,9 @@ function applyZoomAndCheck(args) {
|
|
|
41
60
|
path.unshift(selector);
|
|
42
61
|
current = parent;
|
|
43
62
|
}
|
|
44
|
-
return path.length > 0
|
|
63
|
+
return path.length > 0
|
|
64
|
+
? path.join(' > ')
|
|
65
|
+
: `[data-index="${elementIndex}"]`;
|
|
45
66
|
}
|
|
46
67
|
function isVisible(element) {
|
|
47
68
|
const style = window.getComputedStyle(element);
|
|
@@ -83,6 +104,7 @@ function applyZoomAndCheck(args) {
|
|
|
83
104
|
clippedElements.push({
|
|
84
105
|
selector: getUniqueSelector(element, index),
|
|
85
106
|
tagName: element.tagName.toLowerCase(),
|
|
107
|
+
...getHtmlSnippet(element),
|
|
86
108
|
scrollWidth,
|
|
87
109
|
clientWidth,
|
|
88
110
|
scrollHeight,
|
|
@@ -104,7 +126,10 @@ function applyZoomAndCheck(args) {
|
|
|
104
126
|
*/
|
|
105
127
|
export async function runZoomCheck(options) {
|
|
106
128
|
const { page, targetUrl: targetUrlOption, viewport = ZOOM_BASE_VIEWPORT, screenshot = false, ...location } = options;
|
|
107
|
-
await page.setViewportSize({
|
|
129
|
+
await page.setViewportSize({
|
|
130
|
+
width: viewport.width,
|
|
131
|
+
height: viewport.height,
|
|
132
|
+
});
|
|
108
133
|
// If a URL is available, navigate at the base viewport (legacy ordering).
|
|
109
134
|
const targetUrl = targetUrlOption ?? process.env.TEST_PAGE;
|
|
110
135
|
if (targetUrl) {
|
|
@@ -113,24 +138,30 @@ export async function runZoomCheck(options) {
|
|
|
113
138
|
const zoomResult = await page.evaluate(applyZoomAndCheck, {
|
|
114
139
|
checkSelector: REFLOW_CHECK_SELECTOR,
|
|
115
140
|
tolerance: ZOOM_CLIP_TOLERANCE,
|
|
141
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
116
142
|
});
|
|
117
|
-
const
|
|
118
|
-
url: page.url(),
|
|
143
|
+
const details = {
|
|
119
144
|
zoomFactor: ZOOM_FACTOR,
|
|
120
145
|
viewport: { width: viewport.width, height: viewport.height },
|
|
121
146
|
...zoomResult,
|
|
122
147
|
};
|
|
148
|
+
const result = buildAuditResult({
|
|
149
|
+
source: 'zoom-200-check',
|
|
150
|
+
url: page.url(),
|
|
151
|
+
details,
|
|
152
|
+
buckets: normalizeZoomCheck(details),
|
|
153
|
+
});
|
|
123
154
|
// Output results
|
|
124
155
|
logAuditHeader('Zoom 200% Check Results', 'WCAG 1.4.4', result.url);
|
|
125
156
|
logSummary({
|
|
126
|
-
'Zoom factor': `${
|
|
127
|
-
'Base viewport': `${
|
|
128
|
-
'Document scroll width': `${
|
|
129
|
-
'Document client width': `${
|
|
130
|
-
'Horizontal scroll':
|
|
131
|
-
'Clipped elements':
|
|
157
|
+
'Zoom factor': `${details.zoomFactor}x`,
|
|
158
|
+
'Base viewport': `${details.viewport.width}x${details.viewport.height}`,
|
|
159
|
+
'Document scroll width': `${details.documentScrollWidth}px`,
|
|
160
|
+
'Document client width': `${details.documentClientWidth}px`,
|
|
161
|
+
'Horizontal scroll': details.hasHorizontalScroll,
|
|
162
|
+
'Clipped elements': details.clippedElements.length,
|
|
132
163
|
});
|
|
133
|
-
logIssueList('Clipped Elements',
|
|
164
|
+
logIssueList('Clipped Elements', details.clippedElements, (el, i) => [
|
|
134
165
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
135
166
|
` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
|
|
136
167
|
` Issue: ${el.issueType}`,
|
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
|
}
|