@a11y-skills/audit 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,60 @@
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
+
16
+ ## 0.3.0
17
+
18
+ **Breaking** — every check now returns (and saves) a single axe-style envelope
19
+ instead of its own ad-hoc shape. The public API is not yet stable in `0.x`, so
20
+ this lands as a minor release.
21
+
22
+ ### Changed (breaking)
23
+
24
+ - All `runXxx()` functions return `AuditCheckResult<TDetails>`: findings are
25
+ normalized into `violations` / `incomplete` / `passes` / `inapplicable`
26
+ rule arrays (axe-style `id` / `impact` / `tags` / `helpUrl` / `nodes`), with
27
+ rule-level counts in `summary`. The former top-level fields (`issues`,
28
+ `failAA`, `overflowingElements`, `clippedElements`, ...) moved unchanged
29
+ under `details`.
30
+ - Classification is conservative: only findings whose detection has no blind
31
+ spot and where no WCAG exception can apply are `violations`
32
+ (on-focus context change, text-spacing clipping, invalid autocomplete
33
+ tokens). Everything else — reflow/zoom overflow, meta refresh, orientation
34
+ lock, missing focus styles, undersized targets — lands in `incomplete`, the
35
+ manual-review queue.
36
+ - Saved JSON files use the same envelope; the JSON Schemas were rewritten
37
+ accordingly (`$defs`-based shared envelope + per-check `details` schemas).
38
+ Pre-0.3.0 result files no longer validate.
39
+ - `saveAuditResult()` no longer appends a disclaimer (the envelope carries it).
40
+ - `runAxeAudit` builds the buckets from the raw axe results: violations and
41
+ incomplete keep their nodes, passes/inapplicable keep rule metadata only;
42
+ `details` records the execution configuration.
43
+
44
+ ### Added
45
+
46
+ - `rule-registry.ts`: per-rule metadata (`sc`, accurate per-SC `wcag*` tags,
47
+ `impact`, `scope`, classification) — target size split into
48
+ `a11y-skills/target-size-minimum` (2.5.8 AA) and
49
+ `a11y-skills/target-size-enhanced` (2.5.5 AAA).
50
+ - Pure normalization mappers (`normalize*`), `buildAuditResult()`, and the
51
+ opt-in `mergeNormalizedResults()` exported from the package root. The
52
+ buckets are re-derivable from a saved result's `details`.
53
+ - Element-level evidence: detail records now carry `html` (outerHTML, capped
54
+ at `HTML_SNIPPET_MAX_LENGTH`) and `htmlTruncated`; focus issues gained
55
+ `selector`. `TargetSizeIssue.exceptionAssessment`
56
+ (`ruled-out`/`possible`/`not-assessed`) drives the violation promotion.
57
+ - Tests: mapper unit tests, merge invariants, and ajv (draft 2020-12) schema
58
+ validation of the produced envelopes.
59
+
6
60
  ## 0.2.0
7
61
 
8
62
  Phase 2 checks added (additive). Still a `0.x` preview — the function API may
package/README.ja.md CHANGED
@@ -67,7 +67,7 @@ test("axe audit", async ({ page }, testInfo) => {
67
67
  // outputFile: "axe-result.json", // 任意で上書き
68
68
  // tags: ["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"],
69
69
  });
70
- expect(result.violationCount).toBe(0);
70
+ expect(result.summary.violationCount).toBe(0);
71
71
  });
72
72
  ```
73
73
 
@@ -86,7 +86,7 @@ test("focus indicators", async ({ browser }, testInfo) => {
86
86
  screenshot: true, // 既定: false
87
87
  // contextOptions: { locale: "ja-JP" }, // browser.newContext() に転送
88
88
  });
89
- expect(result.elementsWithoutFocusStyle).toBe(0);
89
+ expect(result.details.elementsWithoutFocusStyle).toBe(0);
90
90
  });
91
91
  ```
92
92
 
@@ -135,6 +135,52 @@ TEST_PAGE=https://example.com A11Y_OUTPUT_DIR=./a11y-results npx playwright test
135
135
  > を `testMatch` に指定してもテストは見つかりません。上記の 1 行 re-export spec が
136
136
  > entry を実行する正式な方法です。
137
137
 
138
+ ## 結果形式
139
+
140
+ すべての検査は同じ axe 風の envelope を返し、同じ形式で JSON に保存します:
141
+
142
+ ```ts
143
+ interface AuditCheckResult<TDetails> {
144
+ source: CheckSource; // 例: "reflow-check"
145
+ url: string;
146
+ timestamp: string;
147
+ violations: NormalizedRuleResult[]; // 確定した違反
148
+ incomplete: NormalizedRuleResult[]; // 要手動確認
149
+ passes: NormalizedRuleResult[]; // 実行して問題が見つからなかったルール
150
+ inapplicable: NormalizedRuleResult[]; // 検査対象がなかったルール
151
+ summary: { violationCount; incompleteCount; passCount; checkedNodes? };
152
+ details: TDetails; // 検査固有の証跡(測定値・スクリーンショット等)
153
+ disclaimer: { ... };
154
+ }
155
+ ```
156
+
157
+ 各ルール結果は axe と同じ形(`id` / `impact` / `description` / `help` /
158
+ `helpUrl` / `tags` / `nodes[]`、`nodes[].target` / `html` / `htmlTruncated` /
159
+ `failureSummary`)です。独自ルールは名前空間付き
160
+ (`a11y-skills/focus-visible`、`a11y-skills/target-size-minimum` など)で、
161
+ SC ごとに正確な WCAG バージョン・レベルタグ(`wcag2aa`、`wcag21aa`、
162
+ `wcag22aa`、`wcag247` 形式の SC タグ)が付きます。
163
+
164
+ **分類は保守的です。** 検出に死角がなく WCAG の例外が適用され得ない場合のみ
165
+ `violations` に入ります(フォーカスによる文脈変化、テキストスペーシングの
166
+ クリップ、autocomplete の不正トークン)。それ以外の検出 — reflow/zoom の
167
+ オーバーフロー、meta refresh、画面方向ロック、フォーカススタイル欠如、
168
+ 小さすぎるターゲット — は `incomplete` に入ります。`incomplete` はノイズでは
169
+ なく「要手動確認キュー」として扱ってください。
170
+
171
+ 同一ページに対する複数検査の結果を 1 つにまとめるには:
172
+
173
+ ```ts
174
+ import { mergeNormalizedResults } from "@a11y-skills/audit";
175
+
176
+ const merged = mergeNormalizedResults([axeResult, reflowResult, targetResult]);
177
+ ```
178
+
179
+ `mergeNormalizedResults` は URL 不一致で例外を投げ、ノードを
180
+ `target` + `failureSummary` で重複排除し、複数バケツに現れるルールは
181
+ `violations > incomplete > passes > inapplicable` の優先順位で統合します。
182
+ frame / shadow root 内の同一セレクタは区別しません。
183
+
138
184
  ## 結果型とスキーマ
139
185
 
140
186
  ```ts
@@ -142,8 +188,10 @@ import type { AxeAuditResult, FocusCheckResult } from "@a11y-skills/audit/schema
142
188
  import { RESULT_SCHEMAS } from "@a11y-skills/audit/schemas";
143
189
  ```
144
190
 
145
- `RESULT_SCHEMAS` は各検査 id に手書きの JSON Schema を対応付けており、`*-result.json`
146
- を実行時に検証できます。
191
+ `RESULT_SCHEMAS` は各検査 id に手書きの JSON Schema(draft 2020-12)を
192
+ 対応付けており、`*-result.json` を実行時に検証できます。正規化マッパー
193
+ (`normalize*`)と `buildAuditResult` はパッケージルートから export されて
194
+ いるため、保存済み結果の `details` から 4 区分をいつでも再導出できます。
147
195
 
148
196
  ## ライセンス
149
197
 
package/README.md CHANGED
@@ -69,7 +69,7 @@ test("axe audit", async ({ page }, testInfo) => {
69
69
  // outputFile: "axe-result.json", // optional override
70
70
  // tags: ["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"],
71
71
  });
72
- expect(result.violationCount).toBe(0);
72
+ expect(result.summary.violationCount).toBe(0);
73
73
  });
74
74
  ```
75
75
 
@@ -88,7 +88,7 @@ test("focus indicators", async ({ browser }, testInfo) => {
88
88
  screenshot: true, // default: false
89
89
  // contextOptions: { locale: "ja-JP" }, // forwarded to browser.newContext()
90
90
  });
91
- expect(result.elementsWithoutFocusStyle).toBe(0);
91
+ expect(result.details.elementsWithoutFocusStyle).toBe(0);
92
92
  });
93
93
  ```
94
94
 
@@ -138,6 +138,52 @@ TEST_PAGE=https://example.com A11Y_OUTPUT_DIR=./a11y-results npx playwright test
138
138
  > `**/node_modules/@a11y-skills/audit/dist/test-entries/*.js` finds no tests.
139
139
  > The one-line re-export specs above are the supported way to run the entries.
140
140
 
141
+ ## Result format
142
+
143
+ Every check returns — and saves as JSON — the same axe-style envelope:
144
+
145
+ ```ts
146
+ interface AuditCheckResult<TDetails> {
147
+ source: CheckSource; // e.g. "reflow-check"
148
+ url: string;
149
+ timestamp: string;
150
+ violations: NormalizedRuleResult[]; // confirmed findings
151
+ incomplete: NormalizedRuleResult[]; // needs manual review
152
+ passes: NormalizedRuleResult[]; // rules that ran and found nothing
153
+ inapplicable: NormalizedRuleResult[]; // nothing to examine
154
+ summary: { violationCount; incompleteCount; passCount; checkedNodes? };
155
+ details: TDetails; // check-specific evidence (measurements, screenshots, ...)
156
+ disclaimer: { ... };
157
+ }
158
+ ```
159
+
160
+ Each rule result is axe-shaped (`id` / `impact` / `description` / `help` /
161
+ `helpUrl` / `tags` / `nodes[]`, with `nodes[].target` / `html` /
162
+ `htmlTruncated` / `failureSummary`). Custom rules are namespaced
163
+ (`a11y-skills/focus-visible`, `a11y-skills/target-size-minimum`, ...) and
164
+ tagged with accurate WCAG version/level tags (`wcag2aa`, `wcag21aa`,
165
+ `wcag22aa`, `wcag247`-style SC tags).
166
+
167
+ **Classification is conservative.** A finding lands in `violations` only when
168
+ the detection has no blind spot and no WCAG exception can apply (on-focus
169
+ context change, text-spacing clipping, invalid autocomplete tokens). All other
170
+ detections — reflow/zoom overflow, meta refresh, orientation lock, missing
171
+ focus styles, undersized targets — are `incomplete`: treat that bucket as the
172
+ manual-review queue, not as noise.
173
+
174
+ To combine several checks for the same page into one view:
175
+
176
+ ```ts
177
+ import { mergeNormalizedResults } from "@a11y-skills/audit";
178
+
179
+ const merged = mergeNormalizedResults([axeResult, reflowResult, targetResult]);
180
+ ```
181
+
182
+ `mergeNormalizedResults` throws on URL mismatches, deduplicates nodes by
183
+ `target` + `failureSummary`, and resolves a rule appearing in several buckets
184
+ by priority (`violations > incomplete > passes > inapplicable`). Identical
185
+ selectors inside different frames or shadow roots are not distinguished.
186
+
141
187
  ## Result types & schemas
142
188
 
143
189
  ```ts
@@ -145,8 +191,11 @@ import type { AxeAuditResult, FocusCheckResult } from "@a11y-skills/audit/schema
145
191
  import { RESULT_SCHEMAS } from "@a11y-skills/audit/schemas";
146
192
  ```
147
193
 
148
- `RESULT_SCHEMAS` maps each check id to a hand-written JSON Schema for validating
149
- the `*-result.json` files at runtime.
194
+ `RESULT_SCHEMAS` maps each check id to a hand-written JSON Schema
195
+ (draft 2020-12) for validating the `*-result.json` files at runtime. The
196
+ normalization mappers (`normalize*`) and `buildAuditResult` are exported from
197
+ the package root, so the buckets can be re-derived from a saved result's
198
+ `details` at any time.
150
199
 
151
200
  ## License
152
201
 
@@ -14,6 +14,8 @@ export declare const AUDIT_DISCLAIMER: {
14
14
  };
15
15
  /** Console disclaimer message */
16
16
  export declare const DISCLAIMER_CONSOLE = "\nNote: Automated testing detects only ~30-40% of WCAG issues.\n Manual testing is required for complete accessibility evaluation.\n";
17
+ /** Maximum length of `NormalizedNode.html` / detail `html` snippets. */
18
+ export declare const HTML_SNIPPET_MAX_LENGTH = 300;
17
19
  /** Default axe-core tag set (WCAG 2.0/2.1/2.2 A & AA) */
18
20
  export declare const DEFAULT_AXE_TAGS: readonly ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"];
19
21
  /** CSS properties to check for focus style changes */
@@ -22,6 +24,8 @@ export declare const FOCUS_STYLE_PROPERTIES: readonly ["outline", "outlineStyle"
22
24
  export declare const FOCUSABLE_SELECTOR: string;
23
25
  /** Extra tab iterations for safety margin */
24
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;
25
29
  /**
26
30
  * Minimum overlap ratio to report as obscured (0-1)
27
31
  * 0.2 means 20% of the focused element must be covered
package/dist/constants.js CHANGED
@@ -21,6 +21,11 @@ Note: Automated testing detects only ~30-40% of WCAG issues.
21
21
  Manual testing is required for complete accessibility evaluation.
22
22
  `;
23
23
  // =============================================================================
24
+ // Normalized result format
25
+ // =============================================================================
26
+ /** Maximum length of `NormalizedNode.html` / detail `html` snippets. */
27
+ export const HTML_SNIPPET_MAX_LENGTH = 300;
28
+ // =============================================================================
24
29
  // Axe Audit defaults (broad WCAG coverage)
25
30
  // =============================================================================
26
31
  /** Default axe-core tag set (WCAG 2.0/2.1/2.2 A & AA) */
@@ -55,6 +60,8 @@ export const FOCUSABLE_SELECTOR = `
55
60
  `.trim();
56
61
  /** Extra tab iterations for safety margin */
57
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;
58
65
  // =============================================================================
59
66
  // Focus Obscured Detection Constants (WCAG 2.4.12)
60
67
  // =============================================================================
@@ -315,18 +322,62 @@ export const AUTOCOMPLETE_FIELD_PATTERNS = {
315
322
  };
316
323
  /** Valid autocomplete token values */
317
324
  export const VALID_AUTOCOMPLETE_TOKENS = [
318
- 'off', 'on',
319
- 'name', 'honorific-prefix', 'given-name', 'additional-name', 'family-name', 'honorific-suffix', 'nickname',
320
- 'email', 'username', 'new-password', 'current-password', 'one-time-code',
321
- 'organization-title', 'organization',
322
- 'street-address', 'address-line1', 'address-line2', 'address-line3',
323
- 'address-level1', 'address-level2', 'address-level3', 'address-level4',
324
- 'country', 'country-name', 'postal-code',
325
- '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',
326
- 'transaction-currency', 'transaction-amount',
327
- 'language', 'bday', 'bday-day', 'bday-month', 'bday-year', 'sex',
328
- 'tel', 'tel-country-code', 'tel-national', 'tel-area-code', 'tel-local', 'tel-local-prefix', 'tel-local-suffix', 'tel-extension',
329
- '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',
330
381
  ];
331
382
  export const DEFAULT_AUTOCOMPLETE_RESULT_FILE = 'autocomplete-result.json';
332
383
  // =============================================================================
@@ -380,28 +431,60 @@ export const DETECTION_RESULT_FILENAME = 'detection-result.json';
380
431
  /** Keywords for pause/stop controls (EN/JP) */
381
432
  export const PAUSE_KEYWORDS = [
382
433
  // English
383
- 'pause', 'stop', 'halt', 'freeze', 'play',
434
+ 'pause',
435
+ 'stop',
436
+ 'halt',
437
+ 'freeze',
438
+ 'play',
384
439
  // Japanese
385
- '一時停止', '停止', 'ポーズ', '止める', '再生',
440
+ '一時停止',
441
+ '停止',
442
+ 'ポーズ',
443
+ '止める',
444
+ '再生',
386
445
  ];
387
446
  /** Class name patterns indicating pause/play controls */
388
447
  export const CONTROL_CLASS_PATTERNS = [
389
- 'pause', 'play', 'stop', 'toggle', 'switch',
390
- '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',
391
457
  ];
392
458
  /** Carousel-related class patterns */
393
459
  export const CAROUSEL_PATTERNS = [
394
- 'carousel', 'slider', 'slide', 'swiper', 'slick',
395
- 'hero', 'banner', 'gallery', 'rotator',
460
+ 'carousel',
461
+ 'slider',
462
+ 'slide',
463
+ 'swiper',
464
+ 'slick',
465
+ 'hero',
466
+ 'banner',
467
+ 'gallery',
468
+ 'rotator',
396
469
  ];
397
470
  /** Navigation control keywords */
398
471
  export const NAV_KEYWORDS = [
399
- 'prev', 'next', '前', '次', 'arrow', 'dot', 'indicator',
472
+ 'prev',
473
+ 'next',
474
+ '前',
475
+ '次',
476
+ 'arrow',
477
+ 'dot',
478
+ 'indicator',
400
479
  ];
401
480
  /** SVG metadata patterns to exclude from accessible names */
402
481
  export const SVG_METADATA_PATTERNS = [
403
- 'created with', 'made with', 'generated by',
404
- 'svg', 'icon', 'symbol',
482
+ 'created with',
483
+ 'made with',
484
+ 'generated by',
485
+ 'svg',
486
+ 'icon',
487
+ 'symbol',
405
488
  ];
406
489
  /** Maximum parent levels to check for carousel context */
407
490
  export const MAX_PARENT_LEVELS = 5;
package/dist/index.d.ts CHANGED
@@ -10,4 +10,6 @@
10
10
  * - `@a11y-skills/audit/schemas`: result types & JSON Schemas.
11
11
  */
12
12
  export * from './types.js';
13
- export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
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';
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
@@ -10,4 +10,6 @@
10
10
  * - `@a11y-skills/audit/schemas`: result types & JSON Schemas.
11
11
  */
12
12
  export * from './types.js';
13
- export { AUDIT_DISCLAIMER, DEFAULT_AXE_TAGS, REFLOW_VIEWPORT, TARGET_SIZE_AA, TARGET_SIZE_AAA, } from './constants.js';
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';
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';
@@ -15,7 +15,8 @@
15
15
  */
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
- import { generateRecommendation, printSummary } from '../utils/recommendations.js';
18
+ import { buildAuditResult, normalizeAutoPlayDetection, } from '../utils/axe-format.js';
19
+ import { generateRecommendation, printSummary, } from '../utils/recommendations.js';
19
20
  /** Capture screenshots at configured intervals. */
20
21
  async function captureScreenshots(page, outputDir) {
21
22
  const screenshots = [];
@@ -114,8 +115,7 @@ export async function runAutoPlayDetection(options) {
114
115
  }
115
116
  pauseVerification = createSkippedVerification(reason);
116
117
  }
117
- const result = {
118
- url: page.url(),
118
+ const details = {
119
119
  screenshotRecords: screenshots,
120
120
  comparisons,
121
121
  hasAutoPlayContent: hasAnyChange,
@@ -129,9 +129,20 @@ export async function runAutoPlayDetection(options) {
129
129
  pauseVerification,
130
130
  }),
131
131
  };
132
+ const result = buildAuditResult({
133
+ source: 'auto-play-detection',
134
+ url: page.url(),
135
+ details,
136
+ buckets: normalizeAutoPlayDetection(details),
137
+ });
132
138
  console.log('\n=== Auto-play Detection Results ===\n');
133
139
  console.log(JSON.stringify(result, null, 2));
134
140
  saveJsonResult(path.join(outputDir, DETECTION_RESULT_FILENAME), result);
135
- printSummary({ hasAutoPlayContent: hasAnyChange, stopsWithin5Seconds, pauseControls, pauseVerification }, outputDir);
141
+ printSummary({
142
+ hasAutoPlayContent: hasAnyChange,
143
+ stopsWithin5Seconds,
144
+ pauseControls,
145
+ pauseVerification,
146
+ }, outputDir);
136
147
  return result;
137
148
  }
@@ -13,13 +13,34 @@
13
13
  * - Cannot confirm actual field purpose; pattern matching is heuristic
14
14
  * - Manual verification needed for edge cases
15
15
  */
16
- import { AUTOCOMPLETE_FIELD_PATTERNS, VALID_AUTOCOMPLETE_TOKENS, DEFAULT_AUTOCOMPLETE_RESULT_FILE, } from '../constants.js';
16
+ import { AUTOCOMPLETE_FIELD_PATTERNS, VALID_AUTOCOMPLETE_TOKENS, DEFAULT_AUTOCOMPLETE_RESULT_FILE, HTML_SNIPPET_MAX_LENGTH, } from '../constants.js';
17
+ import { buildAuditResult, normalizeAutocompleteAudit, } from '../utils/axe-format.js';
17
18
  import { saveAuditResult, logAuditHeader, logSummary, logIssueList, logOutputPaths, } from '../utils/test-harness.js';
18
19
  /**
19
20
  * Collect basic form field information in browser context.
20
21
  * Accessible names are retrieved separately via ariaSnapshot().
21
22
  */
22
- function collectBasicFieldInfo() {
23
+ function collectBasicFieldInfo(args) {
24
+ const { htmlSnippetMaxLength } = args;
25
+ function getHtmlSnippet(element) {
26
+ let html = '';
27
+ try {
28
+ html = element.outerHTML || '';
29
+ }
30
+ catch {
31
+ html = '';
32
+ }
33
+ if (!html) {
34
+ return {
35
+ html: `<${element.tagName.toLowerCase()}>`,
36
+ htmlTruncated: false,
37
+ };
38
+ }
39
+ if (html.length > htmlSnippetMaxLength) {
40
+ return { html: html.slice(0, htmlSnippetMaxLength), htmlTruncated: true };
41
+ }
42
+ return { html, htmlTruncated: false };
43
+ }
23
44
  function getUniqueSelector(element, elementIndex) {
24
45
  if (element.id) {
25
46
  return `#${element.id}`;
@@ -36,7 +57,9 @@ function collectBasicFieldInfo() {
36
57
  path.unshift(selector);
37
58
  current = parent;
38
59
  }
39
- return path.length > 0 ? path.join(' > ') : `[data-index="${elementIndex}"]`;
60
+ return path.length > 0
61
+ ? path.join(' > ')
62
+ : `[data-index="${elementIndex}"]`;
40
63
  }
41
64
  const skipTypes = ['hidden', 'submit', 'reset', 'button', 'image', 'file'];
42
65
  const fields = [];
@@ -51,6 +74,7 @@ function collectBasicFieldInfo() {
51
74
  fields.push({
52
75
  selector: getUniqueSelector(element, index),
53
76
  tagName: el.tagName.toLowerCase(),
77
+ ...getHtmlSnippet(element),
54
78
  inputType,
55
79
  name: el.name || null,
56
80
  id: el.id || null,
@@ -106,6 +130,8 @@ function analyzeFields(fields, patterns, validTokens) {
106
130
  missing.push({
107
131
  selector: field.selector,
108
132
  tagName: field.tagName,
133
+ html: field.html,
134
+ htmlTruncated: field.htmlTruncated,
109
135
  inputType: field.inputType,
110
136
  name: field.name,
111
137
  id: field.id,
@@ -124,6 +150,8 @@ function analyzeFields(fields, patterns, validTokens) {
124
150
  invalid.push({
125
151
  selector: field.selector,
126
152
  tagName: field.tagName,
153
+ html: field.html,
154
+ htmlTruncated: field.htmlTruncated,
127
155
  inputType: field.inputType,
128
156
  name: field.name,
129
157
  id: field.id,
@@ -144,7 +172,9 @@ function analyzeFields(fields, patterns, validTokens) {
144
172
  export async function runAutocompleteAudit(options) {
145
173
  const { page, ...location } = options;
146
174
  // Collect basic field info from DOM
147
- const basicFields = await page.evaluate(collectBasicFieldInfo);
175
+ const basicFields = await page.evaluate(collectBasicFieldInfo, {
176
+ htmlSnippetMaxLength: HTML_SNIPPET_MAX_LENGTH,
177
+ });
148
178
  // Enhance with accessible names via ariaSnapshot()
149
179
  const fields = [];
150
180
  for (const basicField of basicFields) {
@@ -164,26 +194,31 @@ export async function runAutocompleteAudit(options) {
164
194
  }
165
195
  const patterns = Object.entries(AUTOCOMPLETE_FIELD_PATTERNS);
166
196
  const { missing, invalid } = analyzeFields(fields, patterns, VALID_AUTOCOMPLETE_TOKENS);
167
- const result = {
168
- url: page.url(),
197
+ const details = {
169
198
  totalFieldsChecked: fields.length,
170
199
  missingAutocomplete: missing,
171
200
  invalidAutocomplete: invalid,
172
201
  };
202
+ const result = buildAuditResult({
203
+ source: 'autocomplete-audit',
204
+ url: page.url(),
205
+ details,
206
+ buckets: normalizeAutocompleteAudit(details),
207
+ });
173
208
  // Output results
174
209
  logAuditHeader('Autocomplete Audit Results', 'WCAG 1.3.5', result.url);
175
210
  logSummary({
176
- 'Total form fields': result.totalFieldsChecked,
177
- 'Fields missing autocomplete': result.missingAutocomplete.length,
178
- 'Fields with invalid autocomplete': result.invalidAutocomplete.length,
211
+ 'Total form fields': details.totalFieldsChecked,
212
+ 'Fields missing autocomplete': details.missingAutocomplete.length,
213
+ 'Fields with invalid autocomplete': details.invalidAutocomplete.length,
179
214
  });
180
- logIssueList('Missing Autocomplete', result.missingAutocomplete, (el, i) => [
215
+ logIssueList('Missing Autocomplete', details.missingAutocomplete, (el, i) => [
181
216
  `${i + 1}. <${el.tagName}> "${el.selector}"`,
182
217
  ` name: ${el.name || 'none'}, id: ${el.id || 'none'}`,
183
218
  ` label: "${el.labelText || 'none'}"`,
184
219
  ` Expected: autocomplete="${el.expectedToken}" (matched by ${el.matchedBy})`,
185
220
  ]);
186
- logIssueList('Invalid Autocomplete', result.invalidAutocomplete, (el, i) => [
221
+ logIssueList('Invalid Autocomplete', details.invalidAutocomplete, (el, i) => [
187
222
  `${i + 1}. <${el.tagName}> "${el.selector}"`,
188
223
  ` Current: autocomplete="${el.currentAutocomplete}"`,
189
224
  ` Expected: autocomplete="${el.expectedToken}"`,
@@ -5,6 +5,10 @@
5
5
  * caller is responsible for navigating the page (e.g. `await page.goto(url)`)
6
6
  * before calling this function.
7
7
  *
8
+ * The normalized buckets are built from the RAW axe results (violations and
9
+ * incomplete keep their nodes; passes/inapplicable keep rule metadata only),
10
+ * and `details` records the execution configuration.
11
+ *
8
12
  * Axe-core cannot detect all accessibility issues — manual testing and the
9
13
  * other checks in this package are still needed for complete coverage.
10
14
  */