@a11y-skills/audit 0.3.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 CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to `@a11y-skills/audit` are documented here. This project
4
4
  adheres to [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## 0.3.1
7
+
8
+ ### Fixed
9
+
10
+ - `runFocusIndicatorCheck`: focus-triggered navigations slower than the
11
+ per-Tab settle window are no longer silently missed. The settle window is
12
+ now configurable via the new `navigationSettleMs` option (default: 50), and
13
+ a `framenavigated` listener additionally catches URL changes that commit and
14
+ revert within a single window. (#26)
15
+
6
16
  ## 0.3.0
7
17
 
8
18
  **Breaking** — every check now returns (and saves) a single axe-style envelope
@@ -24,6 +24,8 @@ export declare const FOCUS_STYLE_PROPERTIES: readonly ["outline", "outlineStyle"
24
24
  export declare const FOCUSABLE_SELECTOR: string;
25
25
  /** Extra tab iterations for safety margin */
26
26
  export declare const EXTRA_TAB_ITERATIONS = 10;
27
+ /** Default ms to wait after each Tab press for a focus-triggered navigation to surface. */
28
+ export declare const DEFAULT_NAVIGATION_SETTLE_MS = 50;
27
29
  /**
28
30
  * Minimum overlap ratio to report as obscured (0-1)
29
31
  * 0.2 means 20% of the focused element must be covered
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', 'on',
324
- 'name', 'honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix', 'nickname',
325
- 'email', 'username', 'new-password', 'current-password', 'one-time-code',
326
- 'organization-title', 'organization',
327
- 'street-address', 'address-line1', 'address-line2', 'address-line3',
328
- 'address-level1', 'address-level2', 'address-level3', 'address-level4',
329
- 'country', 'country-name', 'postal-code',
330
- 'cc-name', 'cc-given-name', 'cc-additional-name', 'cc-family-name', 'cc-number', 'cc-exp', 'cc-exp-month', 'cc-exp-year', 'cc-csc', 'cc-type',
331
- 'transaction-currency', 'transaction-amount',
332
- 'language', 'bday', 'bday-day', 'bday-month', 'bday-year', 'sex',
333
- 'tel', 'tel-country-code', 'tel-national', 'tel-area-code', 'tel-local', 'tel-local-prefix', 'tel-local-suffix', 'tel-extension',
334
- 'impp', 'url', 'photo',
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', 'stop', 'halt', 'freeze', 'play',
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', 'play', 'stop', 'toggle', 'switch',
395
- 'control', 'btn-pause', 'btn-play', 'btn-stop',
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', 'slider', 'slide', 'swiper', 'slick',
400
- 'hero', 'banner', 'gallery', 'rotator',
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', 'next', '前', '次', 'arrow', 'dot', 'indicator',
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', 'made with', 'generated by',
409
- 'svg', 'icon', 'symbol',
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';
@@ -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({ hasAutoPlayContent: hasAnyChange, stopsWithin5Seconds, pauseControls, pauseVerification }, outputDir);
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 { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
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 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
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 ? html.slice(0, htmlSnippetMaxLength) : html,
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(50);
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
- if (urlAfterTab !== urlBeforeTab) {
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: urlAfterTab,
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: ${urlAfterTab}`);
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 && overlaps.length > 0) {
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
- // Create overlay container
720
- createOverlay();
721
- const elements = [...document.querySelectorAll(focusableSelector)]
722
- .filter(isVisible)
723
- .filter((el) => !shouldSkip(el));
724
- elements.forEach((el) => {
725
- baseStyles.set(el, captureStyle(el));
726
- elementSelectors.set(el, getSelector(el));
727
- });
728
- return elements.length;
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({ width: viewport.width, height: viewport.height });
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 { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
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 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
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 { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
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 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
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 { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
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 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
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({ type: 'setTimeout', delayMs: actualDelay, callStack });
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({ type: 'setInterval', delayMs: actualDelay, callStack });
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 = 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 { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
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 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
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 { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
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 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
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({ width: viewport.width, height: viewport.height });
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) {
@@ -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: ['totalFieldsChecked', 'missingAutocomplete', 'invalidAutocomplete'],
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: ['metaRefresh', 'timers', 'countdownIndicators', 'hasTimeLimits'],
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' } },
@@ -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' ? buckets.violations : buckets.incomplete).push(ruleResult(key, nodes));
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 { target: ['html'], html: '<html>', htmlTruncated: false, failureSummary };
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' ? details.landscape : details.portrait;
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
  : '';
@@ -17,7 +17,10 @@ export function createLayoutChecker(options) {
17
17
  html = '';
18
18
  }
19
19
  if (!html) {
20
- return { html: `<${element.tagName.toLowerCase()}>`, htmlTruncated: false };
20
+ return {
21
+ html: `<${element.tagName.toLowerCase()}>`,
22
+ htmlTruncated: false,
23
+ };
21
24
  }
22
25
  if (html.length > htmlSnippetMaxLength) {
23
26
  return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
@@ -47,7 +50,9 @@ export function createLayoutChecker(options) {
47
50
  current = parent;
48
51
  }
49
52
  // Append element index as fallback for guaranteed uniqueness
50
- return path.length > 0 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
53
+ return path.length > 0
54
+ ? path.join(' > ')
55
+ : `[data-index="${elementIndex}"]`;
51
56
  }
52
57
  function isAllowedOverflow(element) {
53
58
  return allowedOverflowSelectors.some((selector) => {
@@ -5,7 +5,7 @@
5
5
  * Generate a recommendation based on detection results.
6
6
  */
7
7
  export function generateRecommendation(ctx) {
8
- const { hasAutoPlayContent, stopsWithin5Seconds, pauseControls, pauseVerification } = ctx;
8
+ const { hasAutoPlayContent, stopsWithin5Seconds, pauseControls, pauseVerification, } = ctx;
9
9
  if (!hasAutoPlayContent) {
10
10
  return 'No auto-playing content detected in viewport.';
11
11
  }
@@ -32,7 +32,7 @@ export function generateRecommendation(ctx) {
32
32
  * Print summary to console.
33
33
  */
34
34
  export function printSummary(ctx, outputDir) {
35
- const { hasAutoPlayContent, stopsWithin5Seconds, pauseControls, pauseVerification } = ctx;
35
+ const { hasAutoPlayContent, stopsWithin5Seconds, pauseControls, pauseVerification, } = ctx;
36
36
  console.log('\n--- Summary ---');
37
37
  if (!hasAutoPlayContent) {
38
38
  console.log('✓ No auto-playing content detected in viewport');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a11y-skills/audit",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Playwright + axe-core based WCAG 2.2 accessibility audit functions (axe, focus indicator, reflow, target size, text spacing, zoom, orientation, autocomplete, time limit, auto-play).",
5
5
  "type": "module",
6
6
  "license": "MIT",