@a11y-skills/audit 0.2.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 +44 -0
- package/README.ja.md +52 -4
- package/README.md +53 -4
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +5 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/playwright/runAutoPlayDetection.js +8 -2
- package/dist/playwright/runAutocompleteAudit.js +40 -10
- 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.js +13 -7
- package/dist/playwright/runReflowCheck.js +18 -11
- package/dist/playwright/runTargetSizeCheck.js +42 -10
- package/dist/playwright/runTextSpacingCheck.js +52 -8
- package/dist/playwright/runTimeLimitDetector.js +36 -11
- package/dist/playwright/runZoomCheck.js +35 -11
- package/dist/schemas/index.d.ts +8 -1
- package/dist/schemas/index.js +388 -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 +361 -0
- package/dist/utils/layout.d.ts +2 -0
- package/dist/utils/layout.js +20 -1
- 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
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* - Manual verification needed for exceptions (e.g., camera apps)
|
|
17
17
|
*/
|
|
18
18
|
import { ORIENTATION_VIEWPORTS, ORIENTATION_LOCK_KEYWORDS, MAIN_CONTENT_SELECTORS, DEFAULT_ORIENTATION_RESULT_FILE, DEFAULT_ORIENTATION_PORTRAIT_SCREENSHOT_FILE, DEFAULT_ORIENTATION_LANDSCAPE_SCREENSHOT_FILE, } from '../constants.js';
|
|
19
|
+
import { buildAuditResult, normalizeOrientationCheck, } from '../utils/axe-format.js';
|
|
19
20
|
import { saveAuditResult, resolveOutputPath, takeAuditScreenshot, resolveScreenshotPath, requireTargetUrl, logAuditHeader, logOutputPaths, } from '../utils/test-harness.js';
|
|
20
21
|
/** Capture orientation state in browser context. */
|
|
21
22
|
function captureOrientationState(args) {
|
|
@@ -137,20 +138,25 @@ export async function runOrientationCheck(options) {
|
|
|
137
138
|
const landscapeHasLock = landscapeState.lockMessageFound || landscapeState.mainContentHidden;
|
|
138
139
|
const hasOrientationLock = portraitHasLock || landscapeHasLock;
|
|
139
140
|
const lockDetectedIn = determineLockLocation(portraitHasLock, landscapeHasLock);
|
|
140
|
-
const
|
|
141
|
-
url: page.url(),
|
|
141
|
+
const details = {
|
|
142
142
|
portrait: portraitState,
|
|
143
143
|
landscape: landscapeState,
|
|
144
144
|
hasOrientationLock,
|
|
145
145
|
lockDetectedIn,
|
|
146
146
|
};
|
|
147
|
+
const result = buildAuditResult({
|
|
148
|
+
source: 'orientation-check',
|
|
149
|
+
url: page.url(),
|
|
150
|
+
details,
|
|
151
|
+
buckets: normalizeOrientationCheck(details),
|
|
152
|
+
});
|
|
147
153
|
// Output results
|
|
148
154
|
logAuditHeader('Orientation Check Results', 'WCAG 1.3.4', result.url);
|
|
149
|
-
logOrientationState('Portrait', ORIENTATION_VIEWPORTS.portrait,
|
|
150
|
-
logOrientationState('Landscape', ORIENTATION_VIEWPORTS.landscape,
|
|
151
|
-
console.log(`\nOrientation lock detected: ${
|
|
152
|
-
if (
|
|
153
|
-
console.log(`Lock detected in: ${
|
|
155
|
+
logOrientationState('Portrait', ORIENTATION_VIEWPORTS.portrait, details.portrait);
|
|
156
|
+
logOrientationState('Landscape', ORIENTATION_VIEWPORTS.landscape, details.landscape);
|
|
157
|
+
console.log(`\nOrientation lock detected: ${details.hasOrientationLock ? 'YES' : 'No'}`);
|
|
158
|
+
if (details.hasOrientationLock) {
|
|
159
|
+
console.log(`Lock detected in: ${details.lockDetectedIn}`);
|
|
154
160
|
}
|
|
155
161
|
const writtenPath = saveAuditResult(result, {
|
|
156
162
|
...location,
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* - Cannot distinguish acceptable horizontal scroll (e.g., data tables)
|
|
14
14
|
* - Does not verify functional reflow for complex widgets
|
|
15
15
|
*/
|
|
16
|
-
import { REFLOW_VIEWPORT, REFLOW_OVERFLOW_TOLERANCE, REFLOW_CHECK_SELECTOR, REFLOW_ALLOWED_OVERFLOW_SELECTORS, DEFAULT_REFLOW_RESULT_FILE, DEFAULT_REFLOW_SCREENSHOT_FILE, } from '../constants.js';
|
|
16
|
+
import { REFLOW_VIEWPORT, REFLOW_OVERFLOW_TOLERANCE, REFLOW_CHECK_SELECTOR, REFLOW_ALLOWED_OVERFLOW_SELECTORS, DEFAULT_REFLOW_RESULT_FILE, DEFAULT_REFLOW_SCREENSHOT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
17
17
|
import { createLayoutChecker } from '../utils/layout.js';
|
|
18
|
+
import { buildAuditResult, normalizeReflowCheck } from '../utils/axe-format.js';
|
|
18
19
|
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
19
20
|
/**
|
|
20
21
|
* Run the reflow check against the current page, write the result JSON
|
|
@@ -28,27 +29,33 @@ export async function runReflowCheck(options) {
|
|
|
28
29
|
overflowTolerance,
|
|
29
30
|
checkSelector: REFLOW_CHECK_SELECTOR,
|
|
30
31
|
allowedOverflowSelectors: [...REFLOW_ALLOWED_OVERFLOW_SELECTORS],
|
|
32
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
31
33
|
});
|
|
32
|
-
const
|
|
33
|
-
url: page.url(),
|
|
34
|
+
const details = {
|
|
34
35
|
viewport: { width: viewport.width, height: viewport.height },
|
|
35
36
|
...layoutResult,
|
|
36
37
|
};
|
|
38
|
+
const result = buildAuditResult({
|
|
39
|
+
source: 'reflow-check',
|
|
40
|
+
url: page.url(),
|
|
41
|
+
details,
|
|
42
|
+
buckets: normalizeReflowCheck(details),
|
|
43
|
+
});
|
|
37
44
|
// Output results
|
|
38
45
|
logAuditHeader('Reflow Check Results', 'WCAG 1.4.10', result.url);
|
|
39
46
|
logSummary({
|
|
40
|
-
Viewport: `${
|
|
41
|
-
'Document scroll width': `${
|
|
42
|
-
'Document client width': `${
|
|
43
|
-
'Horizontal scroll':
|
|
44
|
-
'Overflowing elements':
|
|
45
|
-
'Clipped text elements':
|
|
47
|
+
Viewport: `${details.viewport.width}x${details.viewport.height}`,
|
|
48
|
+
'Document scroll width': `${details.documentScrollWidth}px`,
|
|
49
|
+
'Document client width': `${details.documentClientWidth}px`,
|
|
50
|
+
'Horizontal scroll': details.hasHorizontalScroll,
|
|
51
|
+
'Overflowing elements': details.overflowingElements.length,
|
|
52
|
+
'Clipped text elements': details.clippedTextElements.length,
|
|
46
53
|
});
|
|
47
|
-
logIssueList('Overflowing Elements',
|
|
54
|
+
logIssueList('Overflowing Elements', details.overflowingElements, (el, i) => [
|
|
48
55
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
49
56
|
` rect.right: ${el.rect.right}px (viewport: ${el.viewportWidth}px)`,
|
|
50
57
|
]);
|
|
51
|
-
logIssueList('Clipped Text Elements',
|
|
58
|
+
logIssueList('Clipped Text Elements', details.clippedTextElements, (el, i) => [
|
|
52
59
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
53
60
|
` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
|
|
54
61
|
` overflow: ${el.overflow}, overflowX: ${el.overflowX}`,
|
|
@@ -15,13 +15,31 @@
|
|
|
15
15
|
* - Cannot detect all redundant target cases
|
|
16
16
|
* - CSS transform may affect measurements
|
|
17
17
|
*/
|
|
18
|
-
import { INTERACTIVE_SELECTOR, TARGET_SIZE_AA, TARGET_SIZE_AAA, INLINE_CONTEXT_TAGS, UA_CONTROLLED_INPUT_TYPES, INLINE_CONTEXT_MIN_TEXT, DEFAULT_TARGET_SIZE_RESULT_FILE, DEFAULT_TARGET_SIZE_SCREENSHOT_FILE, } from '../constants.js';
|
|
18
|
+
import { INTERACTIVE_SELECTOR, TARGET_SIZE_AA, TARGET_SIZE_AAA, INLINE_CONTEXT_TAGS, UA_CONTROLLED_INPUT_TYPES, INLINE_CONTEXT_MIN_TEXT, DEFAULT_TARGET_SIZE_RESULT_FILE, DEFAULT_TARGET_SIZE_SCREENSHOT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
19
|
+
import { buildAuditResult, normalizeTargetSizeCheck, } from '../utils/axe-format.js';
|
|
19
20
|
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
20
21
|
import { addPageAnnotations } from '../utils/annotations.js';
|
|
21
22
|
/**
|
|
22
23
|
* Collect basic target information from DOM (runs in browser context).
|
|
23
24
|
*/
|
|
24
|
-
function collectBasicTargetInfo(
|
|
25
|
+
function collectBasicTargetInfo(args) {
|
|
26
|
+
const { interactiveSelector, htmlSnippetMaxLength } = args;
|
|
27
|
+
function getHtmlSnippet(element) {
|
|
28
|
+
let html = '';
|
|
29
|
+
try {
|
|
30
|
+
html = element.outerHTML || '';
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
html = '';
|
|
34
|
+
}
|
|
35
|
+
if (!html) {
|
|
36
|
+
return { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
|
|
37
|
+
}
|
|
38
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
39
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
40
|
+
}
|
|
41
|
+
return { html, htmlTruncated: false };
|
|
42
|
+
}
|
|
25
43
|
function getUniqueSelector(element, elementIndex) {
|
|
26
44
|
if (element.id) {
|
|
27
45
|
return `#${CSS.escape(element.id)}`;
|
|
@@ -72,6 +90,7 @@ function collectBasicTargetInfo(interactiveSelector) {
|
|
|
72
90
|
targets.push({
|
|
73
91
|
selector: getUniqueSelector(element, index),
|
|
74
92
|
tagName,
|
|
93
|
+
...getHtmlSnippet(element),
|
|
75
94
|
role,
|
|
76
95
|
width: Math.round(rect.width * 100) / 100,
|
|
77
96
|
height: Math.round(rect.height * 100) / 100,
|
|
@@ -247,6 +266,8 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
|
|
|
247
266
|
const issue = {
|
|
248
267
|
selector: target.selector,
|
|
249
268
|
tagName: target.tagName,
|
|
269
|
+
html: target.html,
|
|
270
|
+
htmlTruncated: target.htmlTruncated,
|
|
250
271
|
role: target.role,
|
|
251
272
|
accessibleName: target.accessibleName,
|
|
252
273
|
width: target.width,
|
|
@@ -255,6 +276,9 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
|
|
|
255
276
|
level,
|
|
256
277
|
exception,
|
|
257
278
|
exceptionDetails,
|
|
279
|
+
// the heuristics can detect exceptions but can never rule out the
|
|
280
|
+
// essential exception, so findings are at best 'possible'/'not-assessed'.
|
|
281
|
+
exceptionAssessment: exception ? 'possible' : 'not-assessed',
|
|
258
282
|
href: target.href,
|
|
259
283
|
};
|
|
260
284
|
if (exception) {
|
|
@@ -276,7 +300,10 @@ function analyzeTargets(targets, aaThreshold, aaaThreshold) {
|
|
|
276
300
|
export async function runTargetSizeCheck(options) {
|
|
277
301
|
const { page, aaThreshold = TARGET_SIZE_AA, aaaThreshold = TARGET_SIZE_AAA, screenshot = false, ...location } = options;
|
|
278
302
|
// Collect basic target info from DOM
|
|
279
|
-
const basicTargets = await page.evaluate(collectBasicTargetInfo,
|
|
303
|
+
const basicTargets = await page.evaluate(collectBasicTargetInfo, {
|
|
304
|
+
interactiveSelector: INTERACTIVE_SELECTOR,
|
|
305
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
306
|
+
});
|
|
280
307
|
// Enhance with accessible names via ariaSnapshot()
|
|
281
308
|
const targets = [];
|
|
282
309
|
for (const basicTarget of basicTargets) {
|
|
@@ -296,8 +323,7 @@ export async function runTargetSizeCheck(options) {
|
|
|
296
323
|
}
|
|
297
324
|
// Analyze targets
|
|
298
325
|
const { failAA, failAAAOnly, passCount, excepted } = analyzeTargets(targets, aaThreshold, aaaThreshold);
|
|
299
|
-
const
|
|
300
|
-
url: page.url(),
|
|
326
|
+
const details = {
|
|
301
327
|
totalTargetsChecked: targets.length,
|
|
302
328
|
failAA,
|
|
303
329
|
failAAAOnly,
|
|
@@ -310,16 +336,22 @@ export async function runTargetSizeCheck(options) {
|
|
|
310
336
|
exceptedCount: excepted.length,
|
|
311
337
|
},
|
|
312
338
|
};
|
|
339
|
+
const result = buildAuditResult({
|
|
340
|
+
source: 'target-size-check',
|
|
341
|
+
url: page.url(),
|
|
342
|
+
details,
|
|
343
|
+
buckets: normalizeTargetSizeCheck(details),
|
|
344
|
+
});
|
|
313
345
|
// Output results
|
|
314
346
|
logAuditHeader('Target Size Check Results', 'WCAG 2.5.5 / 2.5.8', result.url);
|
|
315
347
|
logSummary({
|
|
316
|
-
'Total targets checked':
|
|
348
|
+
'Total targets checked': details.totalTargetsChecked,
|
|
317
349
|
});
|
|
318
350
|
console.log('\nSummary:');
|
|
319
|
-
console.log(` Pass (>= ${aaaThreshold}px): ${
|
|
320
|
-
console.log(` Fail AAA only (${aaThreshold}-${aaaThreshold - 1}px): ${
|
|
321
|
-
console.log(` Fail AA (< ${aaThreshold}px): ${
|
|
322
|
-
console.log(` Possible exceptions: ${
|
|
351
|
+
console.log(` Pass (>= ${aaaThreshold}px): ${details.summary.passCount}`);
|
|
352
|
+
console.log(` Fail AAA only (${aaThreshold}-${aaaThreshold - 1}px): ${details.summary.failAAAOnlyCount}`);
|
|
353
|
+
console.log(` Fail AA (< ${aaThreshold}px): ${details.summary.failAACount}`);
|
|
354
|
+
console.log(` Possible exceptions: ${details.summary.exceptedCount}`);
|
|
323
355
|
logIssueList(`Fail AA (< ${aaThreshold}px) - Requires Fix`, failAA, (el, i) => {
|
|
324
356
|
const lines = [
|
|
325
357
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
@@ -7,11 +7,28 @@
|
|
|
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 { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
|
|
26
|
+
}
|
|
27
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
28
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
29
|
+
}
|
|
30
|
+
return { html, htmlTruncated: false };
|
|
31
|
+
}
|
|
15
32
|
function getUniqueSelector(element, elementIndex) {
|
|
16
33
|
if (element.id) {
|
|
17
34
|
return `#${element.id}`;
|
|
@@ -56,6 +73,7 @@ function collectElementMetrics(args) {
|
|
|
56
73
|
metrics.push({
|
|
57
74
|
selector: getUniqueSelector(element, index),
|
|
58
75
|
tagName: element.tagName.toLowerCase(),
|
|
76
|
+
...getHtmlSnippet(element),
|
|
59
77
|
scrollWidth: element.scrollWidth,
|
|
60
78
|
scrollHeight: element.scrollHeight,
|
|
61
79
|
clientWidth: element.clientWidth,
|
|
@@ -70,13 +88,29 @@ function collectElementMetrics(args) {
|
|
|
70
88
|
}
|
|
71
89
|
/** Inject text spacing CSS and re-collect metrics (browser context). */
|
|
72
90
|
function injectSpacingAndCollect(args) {
|
|
73
|
-
const { css, checkSelector } = args;
|
|
91
|
+
const { css, checkSelector, htmlSnippetMaxLength } = args;
|
|
74
92
|
const styleEl = document.createElement('style');
|
|
75
93
|
styleEl.id = 'wcag-text-spacing-override';
|
|
76
94
|
styleEl.textContent = css;
|
|
77
95
|
document.head.appendChild(styleEl);
|
|
78
96
|
// Force reflow
|
|
79
97
|
void document.body.offsetHeight;
|
|
98
|
+
function getHtmlSnippet(element) {
|
|
99
|
+
let html = '';
|
|
100
|
+
try {
|
|
101
|
+
html = element.outerHTML || '';
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
html = '';
|
|
105
|
+
}
|
|
106
|
+
if (!html) {
|
|
107
|
+
return { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
|
|
108
|
+
}
|
|
109
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
110
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
111
|
+
}
|
|
112
|
+
return { html, htmlTruncated: false };
|
|
113
|
+
}
|
|
80
114
|
function getUniqueSelector(element, elementIndex) {
|
|
81
115
|
if (element.id) {
|
|
82
116
|
return `#${element.id}`;
|
|
@@ -121,6 +155,7 @@ function injectSpacingAndCollect(args) {
|
|
|
121
155
|
metrics.push({
|
|
122
156
|
selector: getUniqueSelector(element, index),
|
|
123
157
|
tagName: element.tagName.toLowerCase(),
|
|
158
|
+
...getHtmlSnippet(element),
|
|
124
159
|
scrollWidth: element.scrollWidth,
|
|
125
160
|
scrollHeight: element.scrollHeight,
|
|
126
161
|
clientWidth: element.clientWidth,
|
|
@@ -175,6 +210,8 @@ function detectClippingIssues(beforeMetrics, afterMetrics, tolerance) {
|
|
|
175
210
|
issues.push({
|
|
176
211
|
selector: after.selector,
|
|
177
212
|
tagName: after.tagName,
|
|
213
|
+
html: after.html,
|
|
214
|
+
htmlTruncated: after.htmlTruncated,
|
|
178
215
|
beforeMetrics: {
|
|
179
216
|
scrollWidth: beforeData.scrollWidth,
|
|
180
217
|
scrollHeight: beforeData.scrollHeight,
|
|
@@ -204,23 +241,30 @@ export async function runTextSpacingCheck(options) {
|
|
|
204
241
|
const { page, tolerance = TEXT_SPACING_CLIP_TOLERANCE, screenshot = false, ...location } = options;
|
|
205
242
|
const beforeMetrics = await page.evaluate(collectElementMetrics, {
|
|
206
243
|
checkSelector: TEXT_SPACING_CHECK_SELECTOR,
|
|
244
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
207
245
|
});
|
|
208
246
|
const afterMetrics = await page.evaluate(injectSpacingAndCollect, {
|
|
209
247
|
css: TEXT_SPACING_CSS,
|
|
210
248
|
checkSelector: TEXT_SPACING_CHECK_SELECTOR,
|
|
249
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
211
250
|
});
|
|
212
251
|
const clippedElements = detectClippingIssues(beforeMetrics, afterMetrics, tolerance);
|
|
213
|
-
const
|
|
214
|
-
url: page.url(),
|
|
252
|
+
const details = {
|
|
215
253
|
clippedElements,
|
|
216
254
|
totalElementsChecked: afterMetrics.length,
|
|
217
255
|
};
|
|
256
|
+
const result = buildAuditResult({
|
|
257
|
+
source: 'text-spacing-check',
|
|
258
|
+
url: page.url(),
|
|
259
|
+
details,
|
|
260
|
+
buckets: normalizeTextSpacingCheck(details),
|
|
261
|
+
});
|
|
218
262
|
logAuditHeader('Text Spacing Check Results', 'WCAG 1.4.12', result.url);
|
|
219
263
|
logSummary({
|
|
220
|
-
'Elements with overflow:hidden checked':
|
|
221
|
-
'Elements with clipping issues':
|
|
264
|
+
'Elements with overflow:hidden checked': details.totalElementsChecked,
|
|
265
|
+
'Elements with clipping issues': details.clippedElements.length,
|
|
222
266
|
});
|
|
223
|
-
logIssueList('Clipped Elements',
|
|
267
|
+
logIssueList('Clipped Elements', details.clippedElements, (el, i) => [
|
|
224
268
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
225
269
|
` Issue: ${el.issueType}`,
|
|
226
270
|
` 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) {
|
|
@@ -60,7 +61,23 @@ function createTimerHookScript(args) {
|
|
|
60
61
|
}
|
|
61
62
|
/** Detect meta refresh + countdown indicators (browser context). */
|
|
62
63
|
function detectTimeLimitIndicators(args) {
|
|
63
|
-
const { keywords } = args;
|
|
64
|
+
const { keywords, htmlSnippetMaxLength } = args;
|
|
65
|
+
function getHtmlSnippet(element) {
|
|
66
|
+
let html = '';
|
|
67
|
+
try {
|
|
68
|
+
html = element.outerHTML || '';
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
html = '';
|
|
72
|
+
}
|
|
73
|
+
if (!html) {
|
|
74
|
+
return { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
|
|
75
|
+
}
|
|
76
|
+
if (html.length > htmlSnippetMaxLength) {
|
|
77
|
+
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
78
|
+
}
|
|
79
|
+
return { html, htmlTruncated: false };
|
|
80
|
+
}
|
|
64
81
|
function getUniqueSelector(element, elementIndex) {
|
|
65
82
|
if (element.id) {
|
|
66
83
|
return `#${element.id}`;
|
|
@@ -91,6 +108,7 @@ function detectTimeLimitIndicators(args) {
|
|
|
91
108
|
content,
|
|
92
109
|
seconds: parseInt(match[1] ?? '0', 10),
|
|
93
110
|
url: match[2]?.trim() || null,
|
|
111
|
+
...getHtmlSnippet(meta),
|
|
94
112
|
});
|
|
95
113
|
}
|
|
96
114
|
}
|
|
@@ -116,6 +134,7 @@ function detectTimeLimitIndicators(args) {
|
|
|
116
134
|
selector: getUniqueSelector(parent, elementIndex),
|
|
117
135
|
text: fullText,
|
|
118
136
|
tagName: parent.tagName.toLowerCase(),
|
|
137
|
+
...getHtmlSnippet(parent),
|
|
119
138
|
});
|
|
120
139
|
}
|
|
121
140
|
}
|
|
@@ -139,25 +158,31 @@ export async function runTimeLimitDetector(options) {
|
|
|
139
158
|
});
|
|
140
159
|
const indicators = await page.evaluate(detectTimeLimitIndicators, {
|
|
141
160
|
keywords: [...TIME_LIMIT_KEYWORDS],
|
|
161
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
142
162
|
});
|
|
143
163
|
const hasTimeLimits = indicators.metaRefresh.length > 0 ||
|
|
144
164
|
timers.length > 0 ||
|
|
145
165
|
indicators.countdownIndicators.length > 0;
|
|
146
|
-
const
|
|
147
|
-
url: page.url(),
|
|
166
|
+
const details = {
|
|
148
167
|
metaRefresh: indicators.metaRefresh,
|
|
149
168
|
timers,
|
|
150
169
|
countdownIndicators: indicators.countdownIndicators,
|
|
151
170
|
hasTimeLimits,
|
|
152
171
|
};
|
|
172
|
+
const result = buildAuditResult({
|
|
173
|
+
source: 'time-limit-detector',
|
|
174
|
+
url: page.url(),
|
|
175
|
+
details,
|
|
176
|
+
buckets: normalizeTimeLimitDetector(details),
|
|
177
|
+
});
|
|
153
178
|
logAuditHeader('Time Limit Detection Results', 'WCAG 2.2.1', result.url);
|
|
154
179
|
logSummary({
|
|
155
|
-
'Meta refresh tags':
|
|
156
|
-
[`Timers detected (${minMs / 1000}s - ${maxMs / 1000}s)`]:
|
|
157
|
-
'Countdown text indicators':
|
|
158
|
-
'Time limits detected':
|
|
180
|
+
'Meta refresh tags': details.metaRefresh.length,
|
|
181
|
+
[`Timers detected (${minMs / 1000}s - ${maxMs / 1000}s)`]: details.timers.length,
|
|
182
|
+
'Countdown text indicators': details.countdownIndicators.length,
|
|
183
|
+
'Time limits detected': details.hasTimeLimits,
|
|
159
184
|
});
|
|
160
|
-
logIssueList('Meta Refresh',
|
|
185
|
+
logIssueList('Meta Refresh', details.metaRefresh, (meta, i) => {
|
|
161
186
|
const lines = [
|
|
162
187
|
`${i + 1}. content="${meta.content}"`,
|
|
163
188
|
` Refresh in ${meta.seconds} seconds`,
|
|
@@ -167,7 +192,7 @@ export async function runTimeLimitDetector(options) {
|
|
|
167
192
|
}
|
|
168
193
|
return lines;
|
|
169
194
|
});
|
|
170
|
-
logIssueList('Detected Timers',
|
|
195
|
+
logIssueList('Detected Timers', details.timers, (timer, i) => {
|
|
171
196
|
const lines = [
|
|
172
197
|
`${i + 1}. ${timer.type} - ${timer.delayMs}ms (${(timer.delayMs / 1000).toFixed(1)}s)`,
|
|
173
198
|
];
|
|
@@ -176,7 +201,7 @@ export async function runTimeLimitDetector(options) {
|
|
|
176
201
|
}
|
|
177
202
|
return lines;
|
|
178
203
|
});
|
|
179
|
-
logIssueList('Countdown Indicators',
|
|
204
|
+
logIssueList('Countdown Indicators', details.countdownIndicators, (indicator, i) => {
|
|
180
205
|
const truncatedText = indicator.text.length > 80
|
|
181
206
|
? indicator.text.slice(0, 80) + '...'
|
|
182
207
|
: indicator.text;
|
|
@@ -15,11 +15,28 @@
|
|
|
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 { 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
|
+
}
|
|
23
40
|
// Apply CSS zoom
|
|
24
41
|
document.documentElement.style.zoom =
|
|
25
42
|
'200%';
|
|
@@ -83,6 +100,7 @@ function applyZoomAndCheck(args) {
|
|
|
83
100
|
clippedElements.push({
|
|
84
101
|
selector: getUniqueSelector(element, index),
|
|
85
102
|
tagName: element.tagName.toLowerCase(),
|
|
103
|
+
...getHtmlSnippet(element),
|
|
86
104
|
scrollWidth,
|
|
87
105
|
clientWidth,
|
|
88
106
|
scrollHeight,
|
|
@@ -113,24 +131,30 @@ export async function runZoomCheck(options) {
|
|
|
113
131
|
const zoomResult = await page.evaluate(applyZoomAndCheck, {
|
|
114
132
|
checkSelector: REFLOW_CHECK_SELECTOR,
|
|
115
133
|
tolerance: ZOOM_CLIP_TOLERANCE,
|
|
134
|
+
htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
|
|
116
135
|
});
|
|
117
|
-
const
|
|
118
|
-
url: page.url(),
|
|
136
|
+
const details = {
|
|
119
137
|
zoomFactor: ZOOM_FACTOR,
|
|
120
138
|
viewport: { width: viewport.width, height: viewport.height },
|
|
121
139
|
...zoomResult,
|
|
122
140
|
};
|
|
141
|
+
const result = buildAuditResult({
|
|
142
|
+
source: 'zoom-200-check',
|
|
143
|
+
url: page.url(),
|
|
144
|
+
details,
|
|
145
|
+
buckets: normalizeZoomCheck(details),
|
|
146
|
+
});
|
|
123
147
|
// Output results
|
|
124
148
|
logAuditHeader('Zoom 200% Check Results', 'WCAG 1.4.4', result.url);
|
|
125
149
|
logSummary({
|
|
126
|
-
'Zoom factor': `${
|
|
127
|
-
'Base viewport': `${
|
|
128
|
-
'Document scroll width': `${
|
|
129
|
-
'Document client width': `${
|
|
130
|
-
'Horizontal scroll':
|
|
131
|
-
'Clipped elements':
|
|
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,
|
|
132
156
|
});
|
|
133
|
-
logIssueList('Clipped Elements',
|
|
157
|
+
logIssueList('Clipped Elements', details.clippedElements, (el, i) => [
|
|
134
158
|
`${i + 1}. <${el.tagName}> "${el.selector}"`,
|
|
135
159
|
` scrollWidth: ${el.scrollWidth}px, clientWidth: ${el.clientWidth}px`,
|
|
136
160
|
` 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
|
}
|