@a11y-skills/audit 0.3.0 → 0.4.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 +23 -0
- package/README.ja.md +64 -0
- package/README.md +64 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +407 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +99 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/playwright/index.js +1 -1
- package/dist/playwright/runAutoPlayDetection.js +7 -2
- package/dist/playwright/runAutocompleteAudit.js +7 -2
- package/dist/playwright/runFocusIndicatorCheck.d.ts +7 -0
- package/dist/playwright/runFocusIndicatorCheck.js +45 -21
- package/dist/playwright/runReflowCheck.js +4 -1
- package/dist/playwright/runTargetSizeCheck.js +8 -3
- package/dist/playwright/runTextSpacingCheck.js +14 -4
- package/dist/playwright/runTimeLimitDetector.js +21 -15
- package/dist/playwright/runZoomCheck.js +12 -5
- package/dist/schemas/index.js +11 -2
- package/dist/utils/axe-format.js +12 -3
- package/dist/utils/layout.js +7 -2
- package/dist/utils/recommendations.js +2 -2
- package/package.json +5 -2
package/dist/constants.js
CHANGED
|
@@ -60,6 +60,8 @@ export const FOCUSABLE_SELECTOR = `
|
|
|
60
60
|
`.trim();
|
|
61
61
|
/** Extra tab iterations for safety margin */
|
|
62
62
|
export const EXTRA_TAB_ITERATIONS = 10;
|
|
63
|
+
/** Default ms to wait after each Tab press for a focus-triggered navigation to surface. */
|
|
64
|
+
export const DEFAULT_NAVIGATION_SETTLE_MS = 50;
|
|
63
65
|
// =============================================================================
|
|
64
66
|
// Focus Obscured Detection Constants (WCAG 2.4.12)
|
|
65
67
|
// =============================================================================
|
|
@@ -320,18 +322,62 @@ export const AUTOCOMPLETE_FIELD_PATTERNS = {
|
|
|
320
322
|
};
|
|
321
323
|
/** Valid autocomplete token values */
|
|
322
324
|
export const VALID_AUTOCOMPLETE_TOKENS = [
|
|
323
|
-
'off',
|
|
324
|
-
'
|
|
325
|
-
'
|
|
326
|
-
'
|
|
327
|
-
'
|
|
328
|
-
'
|
|
329
|
-
'
|
|
330
|
-
'
|
|
331
|
-
'
|
|
332
|
-
'
|
|
333
|
-
'
|
|
334
|
-
'
|
|
325
|
+
'off',
|
|
326
|
+
'on',
|
|
327
|
+
'name',
|
|
328
|
+
'honorific-prefix',
|
|
329
|
+
'given-name',
|
|
330
|
+
'additional-name',
|
|
331
|
+
'family-name',
|
|
332
|
+
'honorific-suffix',
|
|
333
|
+
'nickname',
|
|
334
|
+
'email',
|
|
335
|
+
'username',
|
|
336
|
+
'new-password',
|
|
337
|
+
'current-password',
|
|
338
|
+
'one-time-code',
|
|
339
|
+
'organization-title',
|
|
340
|
+
'organization',
|
|
341
|
+
'street-address',
|
|
342
|
+
'address-line1',
|
|
343
|
+
'address-line2',
|
|
344
|
+
'address-line3',
|
|
345
|
+
'address-level1',
|
|
346
|
+
'address-level2',
|
|
347
|
+
'address-level3',
|
|
348
|
+
'address-level4',
|
|
349
|
+
'country',
|
|
350
|
+
'country-name',
|
|
351
|
+
'postal-code',
|
|
352
|
+
'cc-name',
|
|
353
|
+
'cc-given-name',
|
|
354
|
+
'cc-additional-name',
|
|
355
|
+
'cc-family-name',
|
|
356
|
+
'cc-number',
|
|
357
|
+
'cc-exp',
|
|
358
|
+
'cc-exp-month',
|
|
359
|
+
'cc-exp-year',
|
|
360
|
+
'cc-csc',
|
|
361
|
+
'cc-type',
|
|
362
|
+
'transaction-currency',
|
|
363
|
+
'transaction-amount',
|
|
364
|
+
'language',
|
|
365
|
+
'bday',
|
|
366
|
+
'bday-day',
|
|
367
|
+
'bday-month',
|
|
368
|
+
'bday-year',
|
|
369
|
+
'sex',
|
|
370
|
+
'tel',
|
|
371
|
+
'tel-country-code',
|
|
372
|
+
'tel-national',
|
|
373
|
+
'tel-area-code',
|
|
374
|
+
'tel-local',
|
|
375
|
+
'tel-local-prefix',
|
|
376
|
+
'tel-local-suffix',
|
|
377
|
+
'tel-extension',
|
|
378
|
+
'impp',
|
|
379
|
+
'url',
|
|
380
|
+
'photo',
|
|
335
381
|
];
|
|
336
382
|
export const DEFAULT_AUTOCOMPLETE_RESULT_FILE = 'autocomplete-result.json';
|
|
337
383
|
// =============================================================================
|
|
@@ -385,28 +431,60 @@ export const DETECTION_RESULT_FILENAME = 'detection-result.json';
|
|
|
385
431
|
/** Keywords for pause/stop controls (EN/JP) */
|
|
386
432
|
export const PAUSE_KEYWORDS = [
|
|
387
433
|
// English
|
|
388
|
-
'pause',
|
|
434
|
+
'pause',
|
|
435
|
+
'stop',
|
|
436
|
+
'halt',
|
|
437
|
+
'freeze',
|
|
438
|
+
'play',
|
|
389
439
|
// Japanese
|
|
390
|
-
'一時停止',
|
|
440
|
+
'一時停止',
|
|
441
|
+
'停止',
|
|
442
|
+
'ポーズ',
|
|
443
|
+
'止める',
|
|
444
|
+
'再生',
|
|
391
445
|
];
|
|
392
446
|
/** Class name patterns indicating pause/play controls */
|
|
393
447
|
export const CONTROL_CLASS_PATTERNS = [
|
|
394
|
-
'pause',
|
|
395
|
-
'
|
|
448
|
+
'pause',
|
|
449
|
+
'play',
|
|
450
|
+
'stop',
|
|
451
|
+
'toggle',
|
|
452
|
+
'switch',
|
|
453
|
+
'control',
|
|
454
|
+
'btn-pause',
|
|
455
|
+
'btn-play',
|
|
456
|
+
'btn-stop',
|
|
396
457
|
];
|
|
397
458
|
/** Carousel-related class patterns */
|
|
398
459
|
export const CAROUSEL_PATTERNS = [
|
|
399
|
-
'carousel',
|
|
400
|
-
'
|
|
460
|
+
'carousel',
|
|
461
|
+
'slider',
|
|
462
|
+
'slide',
|
|
463
|
+
'swiper',
|
|
464
|
+
'slick',
|
|
465
|
+
'hero',
|
|
466
|
+
'banner',
|
|
467
|
+
'gallery',
|
|
468
|
+
'rotator',
|
|
401
469
|
];
|
|
402
470
|
/** Navigation control keywords */
|
|
403
471
|
export const NAV_KEYWORDS = [
|
|
404
|
-
'prev',
|
|
472
|
+
'prev',
|
|
473
|
+
'next',
|
|
474
|
+
'前',
|
|
475
|
+
'次',
|
|
476
|
+
'arrow',
|
|
477
|
+
'dot',
|
|
478
|
+
'indicator',
|
|
405
479
|
];
|
|
406
480
|
/** SVG metadata patterns to exclude from accessible names */
|
|
407
481
|
export const SVG_METADATA_PATTERNS = [
|
|
408
|
-
'created with',
|
|
409
|
-
'
|
|
482
|
+
'created with',
|
|
483
|
+
'made with',
|
|
484
|
+
'generated by',
|
|
485
|
+
'svg',
|
|
486
|
+
'icon',
|
|
487
|
+
'symbol',
|
|
410
488
|
];
|
|
411
489
|
/** Maximum parent levels to check for carousel context */
|
|
412
490
|
export const MAX_PARENT_LEVELS = 5;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export * from './types.js';
|
|
13
13
|
export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, HTML_SNIPPET_MAX_LENGTH, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
|
|
14
|
-
export { RULES, getRule, type RuleKey, type RuleMeta } from './utils/rule-registry.js';
|
|
14
|
+
export { RULES, getRule, type RuleKey, type RuleMeta, } from './utils/rule-registry.js';
|
|
15
15
|
export { buildAuditResult, mergeNormalizedResults, normalizeAxeResults, normalizeAutocompleteAudit, normalizeAutoPlayDetection, normalizeFocusCheck, normalizeOrientationCheck, normalizeReflowCheck, normalizeTargetSizeCheck, normalizeTextSpacingCheck, normalizeTimeLimitDetector, normalizeZoomCheck, type MergedAuditResult, type NormalizedBuckets, type RawAxeResults, type RawAxeRule, } from './utils/axe-format.js';
|
package/dist/index.js
CHANGED
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export * from './types.js';
|
|
13
13
|
export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, HTML_SNIPPET_MAX_LENGTH, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
|
|
14
|
-
export { RULES, getRule } from './utils/rule-registry.js';
|
|
14
|
+
export { RULES, getRule, } from './utils/rule-registry.js';
|
|
15
15
|
export { buildAuditResult, mergeNormalizedResults, normalizeAxeResults, normalizeAutocompleteAudit, normalizeAutoPlayDetection, normalizeFocusCheck, normalizeOrientationCheck, normalizeReflowCheck, normalizeTargetSizeCheck, normalizeTextSpacingCheck, normalizeTimeLimitDetector, normalizeZoomCheck, } from './utils/axe-format.js';
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export { runAxeAudit, type RunAxeAuditOptions } from './runAxeAudit.js';
|
|
16
16
|
export { runFocusIndicatorCheck, type RunFocusIndicatorCheckOptions, } from './runFocusIndicatorCheck.js';
|
|
17
|
-
export { runReflowCheck, type RunReflowCheckOptions } from './runReflowCheck.js';
|
|
17
|
+
export { runReflowCheck, type RunReflowCheckOptions, } from './runReflowCheck.js';
|
|
18
18
|
export { runTargetSizeCheck, type RunTargetSizeCheckOptions, } from './runTargetSizeCheck.js';
|
|
19
19
|
export { runTextSpacingCheck, type RunTextSpacingCheckOptions, } from './runTextSpacingCheck.js';
|
|
20
20
|
export { runZoomCheck, type RunZoomCheckOptions } from './runZoomCheck.js';
|
package/dist/playwright/index.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
export { runAxeAudit } from './runAxeAudit.js';
|
|
16
16
|
export { runFocusIndicatorCheck, } from './runFocusIndicatorCheck.js';
|
|
17
|
-
export { runReflowCheck } from './runReflowCheck.js';
|
|
17
|
+
export { runReflowCheck, } from './runReflowCheck.js';
|
|
18
18
|
export { runTargetSizeCheck, } from './runTargetSizeCheck.js';
|
|
19
19
|
export { runTextSpacingCheck, } from './runTextSpacingCheck.js';
|
|
20
20
|
export { runZoomCheck } from './runZoomCheck.js';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import { SCREENSHOT_INTERVALS, CHANGE_THRESHOLD, DEFAULT_AUTO_PLAY_OUTPUT_DIR, DETECTION_RESULT_FILENAME, } from '../constants.js';
|
|
18
18
|
import { buildAuditResult, normalizeAutoPlayDetection, } from '../utils/axe-format.js';
|
|
19
|
-
import { generateRecommendation, printSummary } from '../utils/recommendations.js';
|
|
19
|
+
import { generateRecommendation, printSummary, } from '../utils/recommendations.js';
|
|
20
20
|
/** Capture screenshots at configured intervals. */
|
|
21
21
|
async function captureScreenshots(page, outputDir) {
|
|
22
22
|
const screenshots = [];
|
|
@@ -138,6 +138,11 @@ export async function runAutoPlayDetection(options) {
|
|
|
138
138
|
console.log('\n=== Auto-play Detection Results ===\n');
|
|
139
139
|
console.log(JSON.stringify(result, null, 2));
|
|
140
140
|
saveJsonResult(path.join(outputDir, DETECTION_RESULT_FILENAME), result);
|
|
141
|
-
printSummary({
|
|
141
|
+
printSummary({
|
|
142
|
+
hasAutoPlayContent: hasAnyChange,
|
|
143
|
+
stopsWithin5Seconds,
|
|
144
|
+
pauseControls,
|
|
145
|
+
pauseVerification,
|
|
146
|
+
}, outputDir);
|
|
142
147
|
return result;
|
|
143
148
|
}
|
|
@@ -31,7 +31,10 @@ function collectBasicFieldInfo(args) {
|
|
|
31
31
|
html = '';
|
|
32
32
|
}
|
|
33
33
|
if (!html) {
|
|
34
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
36
|
+
htmlTruncated: false,
|
|
37
|
+
};
|
|
35
38
|
}
|
|
36
39
|
if (html.length > htmlSnippetMaxLength) {
|
|
37
40
|
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
@@ -54,7 +57,9 @@ function collectBasicFieldInfo(args) {
|
|
|
54
57
|
path.unshift(selector);
|
|
55
58
|
current = parent;
|
|
56
59
|
}
|
|
57
|
-
return path.length > 0
|
|
60
|
+
return path.length > 0
|
|
61
|
+
? path.join(' > ')
|
|
62
|
+
: `[data-index="${elementIndex}"]`;
|
|
58
63
|
}
|
|
59
64
|
const skipTypes = ['hidden', 'submit', 'reset', 'button', 'image', 'file'];
|
|
60
65
|
const fields = [];
|
|
@@ -22,6 +22,13 @@ export interface RunFocusIndicatorCheckOptions extends OutputLocationOptions {
|
|
|
22
22
|
screenshot?: boolean;
|
|
23
23
|
/** Options forwarded to `browser.newContext()` (locale, viewport, storageState, ...). */
|
|
24
24
|
contextOptions?: BrowserContextOptions;
|
|
25
|
+
/**
|
|
26
|
+
* Milliseconds to wait after each Tab press for a focus-triggered navigation
|
|
27
|
+
* to surface (default: 50). A navigation that fires after this window may be
|
|
28
|
+
* missed or attributed to a neighboring element — raise this value when
|
|
29
|
+
* auditing pages with debounced or otherwise delayed focus handlers.
|
|
30
|
+
*/
|
|
31
|
+
navigationSettleMs?: number;
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
27
34
|
* Run the focus indicator check, write the result JSON (and optionally a
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The context this function creates is closed before it returns.
|
|
12
12
|
*/
|
|
13
|
-
import { FOCUSABLE_SELECTOR, FOCUS_STYLE_PROPERTIES, EXTRA_TAB_ITERATIONS, DEFAULT_FOCUS_RESULT_FILE, DEFAULT_FOCUS_SCREENSHOT_FILE, FOCUS_OBSCURED_MIN_OVERLAP_RATIO, FOCUS_OBSCURED_MIN_OVERLAP_PX, FOCUS_OBSCURED_EXCLUDE_SELECTORS, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
13
|
+
import { FOCUSABLE_SELECTOR, FOCUS_STYLE_PROPERTIES, EXTRA_TAB_ITERATIONS, DEFAULT_NAVIGATION_SETTLE_MS, DEFAULT_FOCUS_RESULT_FILE, DEFAULT_FOCUS_SCREENSHOT_FILE, FOCUS_OBSCURED_MIN_OVERLAP_RATIO, FOCUS_OBSCURED_MIN_OVERLAP_PX, FOCUS_OBSCURED_EXCLUDE_SELECTORS, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
|
|
14
14
|
import { buildAuditResult, normalizeFocusCheck } from '../utils/axe-format.js';
|
|
15
15
|
import { resolveOutputPath, resolveScreenshotPath, saveAuditResult, takeAuditScreenshot, requireTargetUrl, logAuditHeader, logOutputPaths, } from '../utils/test-harness.js';
|
|
16
16
|
// =============================================================================
|
|
@@ -73,7 +73,7 @@ const MAX_RETRIES = 5;
|
|
|
73
73
|
* screenshot), and return the parsed result.
|
|
74
74
|
*/
|
|
75
75
|
export async function runFocusIndicatorCheck(options) {
|
|
76
|
-
const { browser, targetUrl: targetUrlOption, screenshot = false, contextOptions, ...location } = options;
|
|
76
|
+
const { browser, targetUrl: targetUrlOption, screenshot = false, contextOptions, navigationSettleMs = DEFAULT_NAVIGATION_SETTLE_MS, ...location } = options;
|
|
77
77
|
const targetUrl = requireTargetUrl(targetUrlOption);
|
|
78
78
|
const resolvedResultPath = resolveOutputPath({
|
|
79
79
|
...location,
|
|
@@ -95,6 +95,18 @@ export async function runFocusIndicatorCheck(options) {
|
|
|
95
95
|
// Create a fresh context for each attempt to reset init scripts
|
|
96
96
|
context = await browser.newContext(contextOptions);
|
|
97
97
|
const page = await context.newPage();
|
|
98
|
+
// Track main-frame navigations (WCAG 3.2.1). Catches URL changes that
|
|
99
|
+
// commit and revert within a single settle window (which the before/after
|
|
100
|
+
// URL diff alone would miss). A navigation landing between iterations is
|
|
101
|
+
// absorbed into the next urlBeforeTab and is NOT caught by this listener —
|
|
102
|
+
// raise navigationSettleMs for pages with slow focus-triggered handlers.
|
|
103
|
+
// Reset after the initial goto; checked alongside the URL diff.
|
|
104
|
+
let lateNavigationUrl = null;
|
|
105
|
+
page.on('framenavigated', (frame) => {
|
|
106
|
+
if (frame === page.mainFrame()) {
|
|
107
|
+
lateNavigationUrl = frame.url();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
98
110
|
const focusHistory = [];
|
|
99
111
|
let lastFocusedElement = null;
|
|
100
112
|
let navigationDetected = false;
|
|
@@ -160,6 +172,9 @@ export async function runFocusIndicatorCheck(options) {
|
|
|
160
172
|
});
|
|
161
173
|
}, skipSelectors);
|
|
162
174
|
}
|
|
175
|
+
// Reset the late-navigation flag so the initial goto and skip-element
|
|
176
|
+
// setup above don't produce false positives in the Tab loop.
|
|
177
|
+
lateNavigationUrl = null;
|
|
163
178
|
// Tab through all elements with navigation detection
|
|
164
179
|
for (let i = 0; i < count + EXTRA_TAB_ITERATIONS; i++) {
|
|
165
180
|
const urlBeforeTab = page.url();
|
|
@@ -202,7 +217,9 @@ export async function runFocusIndicatorCheck(options) {
|
|
|
202
217
|
el.textContent?.slice(0, 30) ||
|
|
203
218
|
'',
|
|
204
219
|
selector: getSelector(el),
|
|
205
|
-
html: htmlTruncated
|
|
220
|
+
html: htmlTruncated
|
|
221
|
+
? html.slice(0, htmlSnippetMaxLength)
|
|
222
|
+
: html,
|
|
206
223
|
htmlTruncated,
|
|
207
224
|
};
|
|
208
225
|
}, HTML_SNIPPET_MAX_LENGTH);
|
|
@@ -212,18 +229,22 @@ export async function runFocusIndicatorCheck(options) {
|
|
|
212
229
|
currentFocusedElement = lastFocusedElement;
|
|
213
230
|
}
|
|
214
231
|
// Small wait to allow any navigation to start
|
|
215
|
-
await page.waitForTimeout(
|
|
216
|
-
// Check if navigation occurred
|
|
232
|
+
await page.waitForTimeout(navigationSettleMs);
|
|
233
|
+
// Check if navigation occurred (either within the settle window or late)
|
|
217
234
|
const urlAfterTab = page.url();
|
|
218
|
-
|
|
235
|
+
const lateNav = lateNavigationUrl !== null && lateNavigationUrl !== urlBeforeTab;
|
|
236
|
+
if (urlAfterTab !== urlBeforeTab || lateNav) {
|
|
219
237
|
// 3.2.1 violation detected!
|
|
220
238
|
navigationDetected = true;
|
|
221
239
|
const culprit = currentFocusedElement || lastFocusedElement;
|
|
222
240
|
if (culprit && !skipSelectors.includes(culprit.selector)) {
|
|
241
|
+
const toUrl = urlAfterTab !== urlBeforeTab
|
|
242
|
+
? urlAfterTab
|
|
243
|
+
: (lateNavigationUrl ?? urlAfterTab);
|
|
223
244
|
onFocusViolations.push({
|
|
224
245
|
element: culprit,
|
|
225
246
|
fromUrl: urlBeforeTab,
|
|
226
|
-
toUrl
|
|
247
|
+
toUrl,
|
|
227
248
|
changeType: 'navigation',
|
|
228
249
|
});
|
|
229
250
|
skipSelectors.push(culprit.selector);
|
|
@@ -231,9 +252,10 @@ export async function runFocusIndicatorCheck(options) {
|
|
|
231
252
|
console.warn(` Element: <${culprit.tag}> "${culprit.name}"`);
|
|
232
253
|
console.warn(` Selector: ${culprit.selector}`);
|
|
233
254
|
console.warn(` From: ${urlBeforeTab}`);
|
|
234
|
-
console.warn(` To: ${
|
|
255
|
+
console.warn(` To: ${toUrl}`);
|
|
235
256
|
console.warn(` Restarting test with this element skipped...`);
|
|
236
257
|
}
|
|
258
|
+
lateNavigationUrl = null;
|
|
237
259
|
break;
|
|
238
260
|
}
|
|
239
261
|
}
|
|
@@ -687,7 +709,8 @@ function createFocusTrackerScript(args) {
|
|
|
687
709
|
// Clamp ratio to max 1.0 to handle overlapping obscurers covering same region
|
|
688
710
|
const obscuredRatio = Math.min(totalOverlapArea / focusedArea, 1.0);
|
|
689
711
|
// Only report if obscured ratio exceeds threshold
|
|
690
|
-
if (obscuredRatio >= obscuredConfig.minOverlapRatio &&
|
|
712
|
+
if (obscuredRatio >= obscuredConfig.minOverlapRatio &&
|
|
713
|
+
overlaps.length > 0) {
|
|
691
714
|
// Add visual annotation
|
|
692
715
|
addAnnotationBox(focusedEl, `⚠ 2.4.12 Obscured (${(obscuredRatio * 100).toFixed(0)}%)`, 'focus-obscured');
|
|
693
716
|
// Report to test
|
|
@@ -715,18 +738,19 @@ function createFocusTrackerScript(args) {
|
|
|
715
738
|
/**
|
|
716
739
|
* Initialize focus tracker - called after page load
|
|
717
740
|
*/
|
|
718
|
-
window.initFocusTracker =
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
741
|
+
window.initFocusTracker =
|
|
742
|
+
() => {
|
|
743
|
+
// Create overlay container
|
|
744
|
+
createOverlay();
|
|
745
|
+
const elements = [...document.querySelectorAll(focusableSelector)]
|
|
746
|
+
.filter(isVisible)
|
|
747
|
+
.filter((el) => !shouldSkip(el));
|
|
748
|
+
elements.forEach((el) => {
|
|
749
|
+
baseStyles.set(el, captureStyle(el));
|
|
750
|
+
elementSelectors.set(el, getSelector(el));
|
|
751
|
+
});
|
|
752
|
+
return elements.length;
|
|
753
|
+
};
|
|
730
754
|
/**
|
|
731
755
|
* Handle focus events
|
|
732
756
|
*/
|
|
@@ -23,7 +23,10 @@ import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHe
|
|
|
23
23
|
*/
|
|
24
24
|
export async function runReflowCheck(options) {
|
|
25
25
|
const { page, viewport = REFLOW_VIEWPORT, overflowTolerance = REFLOW_OVERFLOW_TOLERANCE, screenshot = false, ...location } = options;
|
|
26
|
-
await page.setViewportSize({
|
|
26
|
+
await page.setViewportSize({
|
|
27
|
+
width: viewport.width,
|
|
28
|
+
height: viewport.height,
|
|
29
|
+
});
|
|
27
30
|
const layoutResult = await page.evaluate(createLayoutChecker, {
|
|
28
31
|
viewportWidth: viewport.width,
|
|
29
32
|
overflowTolerance,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
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
19
|
import { buildAuditResult, normalizeTargetSizeCheck, } from '../utils/axe-format.js';
|
|
20
20
|
import { saveAuditResult, takeAuditScreenshot, resolveScreenshotPath, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
|
|
21
|
-
import { addPageAnnotations } from '../utils/annotations.js';
|
|
21
|
+
import { addPageAnnotations, } from '../utils/annotations.js';
|
|
22
22
|
/**
|
|
23
23
|
* Collect basic target information from DOM (runs in browser context).
|
|
24
24
|
*/
|
|
@@ -33,7 +33,10 @@ function collectBasicTargetInfo(args) {
|
|
|
33
33
|
html = '';
|
|
34
34
|
}
|
|
35
35
|
if (!html) {
|
|
36
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
38
|
+
htmlTruncated: false,
|
|
39
|
+
};
|
|
37
40
|
}
|
|
38
41
|
if (html.length > htmlSnippetMaxLength) {
|
|
39
42
|
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
@@ -59,7 +62,9 @@ function collectBasicTargetInfo(args) {
|
|
|
59
62
|
path.unshift(selector);
|
|
60
63
|
current = parent;
|
|
61
64
|
}
|
|
62
|
-
return path.length > 0
|
|
65
|
+
return path.length > 0
|
|
66
|
+
? path.join(' > ')
|
|
67
|
+
: `[data-index="${elementIndex}"]`;
|
|
63
68
|
}
|
|
64
69
|
const targets = [];
|
|
65
70
|
const elements = document.querySelectorAll(interactiveSelector);
|
|
@@ -22,7 +22,10 @@ function collectElementMetrics(args) {
|
|
|
22
22
|
html = '';
|
|
23
23
|
}
|
|
24
24
|
if (!html) {
|
|
25
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
27
|
+
htmlTruncated: false,
|
|
28
|
+
};
|
|
26
29
|
}
|
|
27
30
|
if (html.length > htmlSnippetMaxLength) {
|
|
28
31
|
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
@@ -45,7 +48,9 @@ function collectElementMetrics(args) {
|
|
|
45
48
|
path.unshift(selector);
|
|
46
49
|
current = parent;
|
|
47
50
|
}
|
|
48
|
-
return path.length > 0
|
|
51
|
+
return path.length > 0
|
|
52
|
+
? path.join(' > ')
|
|
53
|
+
: `[data-index="${elementIndex}"]`;
|
|
49
54
|
}
|
|
50
55
|
function isVisible(element) {
|
|
51
56
|
const style = window.getComputedStyle(element);
|
|
@@ -104,7 +109,10 @@ function injectSpacingAndCollect(args) {
|
|
|
104
109
|
html = '';
|
|
105
110
|
}
|
|
106
111
|
if (!html) {
|
|
107
|
-
return {
|
|
112
|
+
return {
|
|
113
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
114
|
+
htmlTruncated: false,
|
|
115
|
+
};
|
|
108
116
|
}
|
|
109
117
|
if (html.length > htmlSnippetMaxLength) {
|
|
110
118
|
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
@@ -127,7 +135,9 @@ function injectSpacingAndCollect(args) {
|
|
|
127
135
|
path.unshift(selector);
|
|
128
136
|
current = parent;
|
|
129
137
|
}
|
|
130
|
-
return path.length > 0
|
|
138
|
+
return path.length > 0
|
|
139
|
+
? path.join(' > ')
|
|
140
|
+
: `[data-index="${elementIndex}"]`;
|
|
131
141
|
}
|
|
132
142
|
function isVisible(element) {
|
|
133
143
|
const style = window.getComputedStyle(element);
|
|
@@ -30,13 +30,13 @@ function createTimerHookScript(args) {
|
|
|
30
30
|
callStack =
|
|
31
31
|
e.stack?.split('\n').slice(2, 5).join('\n') || null;
|
|
32
32
|
}
|
|
33
|
-
capturedTimers.push({
|
|
33
|
+
capturedTimers.push({
|
|
34
|
+
type: 'setTimeout',
|
|
35
|
+
delayMs: actualDelay,
|
|
36
|
+
callStack,
|
|
37
|
+
});
|
|
34
38
|
}
|
|
35
|
-
return originalSetTimeout.apply(window, [
|
|
36
|
-
callback,
|
|
37
|
-
delay,
|
|
38
|
-
...rest,
|
|
39
|
-
]);
|
|
39
|
+
return originalSetTimeout.apply(window, [callback, delay, ...rest]);
|
|
40
40
|
};
|
|
41
41
|
window.setInterval = function (callback, delay, ...rest) {
|
|
42
42
|
const actualDelay = delay || 0;
|
|
@@ -49,15 +49,16 @@ function createTimerHookScript(args) {
|
|
|
49
49
|
callStack =
|
|
50
50
|
e.stack?.split('\n').slice(2, 5).join('\n') || null;
|
|
51
51
|
}
|
|
52
|
-
capturedTimers.push({
|
|
52
|
+
capturedTimers.push({
|
|
53
|
+
type: 'setInterval',
|
|
54
|
+
delayMs: actualDelay,
|
|
55
|
+
callStack,
|
|
56
|
+
});
|
|
53
57
|
}
|
|
54
|
-
return originalSetInterval.apply(window, [
|
|
55
|
-
callback,
|
|
56
|
-
delay,
|
|
57
|
-
...rest,
|
|
58
|
-
]);
|
|
58
|
+
return originalSetInterval.apply(window, [callback, delay, ...rest]);
|
|
59
59
|
};
|
|
60
|
-
window.__capturedTimers =
|
|
60
|
+
window.__capturedTimers =
|
|
61
|
+
capturedTimers;
|
|
61
62
|
}
|
|
62
63
|
/** Detect meta refresh + countdown indicators (browser context). */
|
|
63
64
|
function detectTimeLimitIndicators(args) {
|
|
@@ -71,7 +72,10 @@ function detectTimeLimitIndicators(args) {
|
|
|
71
72
|
html = '';
|
|
72
73
|
}
|
|
73
74
|
if (!html) {
|
|
74
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
77
|
+
htmlTruncated: false,
|
|
78
|
+
};
|
|
75
79
|
}
|
|
76
80
|
if (html.length > htmlSnippetMaxLength) {
|
|
77
81
|
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
@@ -94,7 +98,9 @@ function detectTimeLimitIndicators(args) {
|
|
|
94
98
|
path.unshift(selector);
|
|
95
99
|
current = parent;
|
|
96
100
|
}
|
|
97
|
-
return path.length > 0
|
|
101
|
+
return path.length > 0
|
|
102
|
+
? path.join(' > ')
|
|
103
|
+
: `[data-index="${elementIndex}"]`;
|
|
98
104
|
}
|
|
99
105
|
const metaRefresh = [];
|
|
100
106
|
const metaTags = document.querySelectorAll('meta[http-equiv="refresh"]');
|
|
@@ -30,7 +30,10 @@ function applyZoomAndCheck(args) {
|
|
|
30
30
|
html = '';
|
|
31
31
|
}
|
|
32
32
|
if (!html) {
|
|
33
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
html: `<${element.tagName.toLowerCase()}>`,
|
|
35
|
+
htmlTruncated: false,
|
|
36
|
+
};
|
|
34
37
|
}
|
|
35
38
|
if (html.length > htmlSnippetMaxLength) {
|
|
36
39
|
return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
|
|
@@ -38,8 +41,7 @@ function applyZoomAndCheck(args) {
|
|
|
38
41
|
return { html, htmlTruncated: false };
|
|
39
42
|
}
|
|
40
43
|
// Apply CSS zoom
|
|
41
|
-
document.documentElement.style.zoom =
|
|
42
|
-
'200%';
|
|
44
|
+
document.documentElement.style.zoom = '200%';
|
|
43
45
|
// Force reflow
|
|
44
46
|
void document.body.offsetHeight;
|
|
45
47
|
function getUniqueSelector(element, elementIndex) {
|
|
@@ -58,7 +60,9 @@ function applyZoomAndCheck(args) {
|
|
|
58
60
|
path.unshift(selector);
|
|
59
61
|
current = parent;
|
|
60
62
|
}
|
|
61
|
-
return path.length > 0
|
|
63
|
+
return path.length > 0
|
|
64
|
+
? path.join(' > ')
|
|
65
|
+
: `[data-index="${elementIndex}"]`;
|
|
62
66
|
}
|
|
63
67
|
function isVisible(element) {
|
|
64
68
|
const style = window.getComputedStyle(element);
|
|
@@ -122,7 +126,10 @@ function applyZoomAndCheck(args) {
|
|
|
122
126
|
*/
|
|
123
127
|
export async function runZoomCheck(options) {
|
|
124
128
|
const { page, targetUrl: targetUrlOption, viewport = ZOOM_BASE_VIEWPORT, screenshot = false, ...location } = options;
|
|
125
|
-
await page.setViewportSize({
|
|
129
|
+
await page.setViewportSize({
|
|
130
|
+
width: viewport.width,
|
|
131
|
+
height: viewport.height,
|
|
132
|
+
});
|
|
126
133
|
// If a URL is available, navigate at the base viewport (legacy ordering).
|
|
127
134
|
const targetUrl = targetUrlOption ?? process.env.TEST_PAGE;
|
|
128
135
|
if (targetUrl) {
|
package/dist/schemas/index.js
CHANGED
|
@@ -399,7 +399,11 @@ export const AUTOCOMPLETE_AUDIT_RESULT_SCHEMA = buildEnvelopeSchema({
|
|
|
399
399
|
source: 'autocomplete-audit',
|
|
400
400
|
details: {
|
|
401
401
|
type: 'object',
|
|
402
|
-
required: [
|
|
402
|
+
required: [
|
|
403
|
+
'totalFieldsChecked',
|
|
404
|
+
'missingAutocomplete',
|
|
405
|
+
'invalidAutocomplete',
|
|
406
|
+
],
|
|
403
407
|
properties: {
|
|
404
408
|
totalFieldsChecked: { type: 'number' },
|
|
405
409
|
missingAutocomplete: { type: 'array', items: { type: 'object' } },
|
|
@@ -413,7 +417,12 @@ export const TIME_LIMIT_DETECTOR_RESULT_SCHEMA = buildEnvelopeSchema({
|
|
|
413
417
|
source: 'time-limit-detector',
|
|
414
418
|
details: {
|
|
415
419
|
type: 'object',
|
|
416
|
-
required: [
|
|
420
|
+
required: [
|
|
421
|
+
'metaRefresh',
|
|
422
|
+
'timers',
|
|
423
|
+
'countdownIndicators',
|
|
424
|
+
'hasTimeLimits',
|
|
425
|
+
],
|
|
417
426
|
properties: {
|
|
418
427
|
metaRefresh: { type: 'array', items: { type: 'object' } },
|
|
419
428
|
timers: { type: 'array', items: { type: 'object' } },
|
package/dist/utils/axe-format.js
CHANGED
|
@@ -40,7 +40,9 @@ function bucketize(buckets, key, nodes, applicable, override) {
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
const classification = override ?? getRule(key).classification;
|
|
43
|
-
(classification === 'violation'
|
|
43
|
+
(classification === 'violation'
|
|
44
|
+
? buckets.violations
|
|
45
|
+
: buckets.incomplete).push(ruleResult(key, nodes));
|
|
44
46
|
}
|
|
45
47
|
/** Build a node from element evidence, synthesizing `html` when missing. */
|
|
46
48
|
function toNode(el, failureSummary) {
|
|
@@ -55,7 +57,12 @@ function toNode(el, failureSummary) {
|
|
|
55
57
|
}
|
|
56
58
|
/** Build a page-level node (`target: ['html']`). */
|
|
57
59
|
function pageNode(failureSummary) {
|
|
58
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
target: ['html'],
|
|
62
|
+
html: '<html>',
|
|
63
|
+
htmlTruncated: false,
|
|
64
|
+
failureSummary,
|
|
65
|
+
};
|
|
59
66
|
}
|
|
60
67
|
function describeElement(ref) {
|
|
61
68
|
return `<${ref.tag.toLowerCase()}> "${ref.name}"`;
|
|
@@ -188,7 +195,9 @@ export function normalizeOrientationCheck(details) {
|
|
|
188
195
|
const buckets = emptyBuckets();
|
|
189
196
|
const nodes = [];
|
|
190
197
|
if (details.hasOrientationLock) {
|
|
191
|
-
const state = details.lockDetectedIn === 'landscape'
|
|
198
|
+
const state = details.lockDetectedIn === 'landscape'
|
|
199
|
+
? details.landscape
|
|
200
|
+
: details.portrait;
|
|
192
201
|
const messagePart = state.lockMessageText
|
|
193
202
|
? ` Lock message found: "${state.lockMessageText}".`
|
|
194
203
|
: '';
|