pour-engine 1.29.0 → 1.30.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.
Files changed (88) hide show
  1. package/README.md +1 -6
  2. package/config/project.config.js +1 -1
  3. package/engine/index.js +15 -16
  4. package/engine/rules/best-practice/accesskeys.js +1 -0
  5. package/engine/rules/best-practice/button-type.js +1 -0
  6. package/engine/rules/best-practice/empty-heading.js +1 -0
  7. package/engine/rules/best-practice/fieldset-legend.js +1 -0
  8. package/engine/rules/best-practice/heading-order.js +1 -0
  9. package/engine/rules/best-practice/landmark-one-main.js +1 -0
  10. package/engine/rules/best-practice/landmark-top-level.js +1 -0
  11. package/engine/rules/best-practice/landmark-unique.js +1 -0
  12. package/engine/rules/best-practice/multiple-labels.js +1 -0
  13. package/engine/rules/best-practice/new-window-link.js +1 -0
  14. package/engine/rules/best-practice/no-autofocus.js +1 -0
  15. package/engine/rules/best-practice/page-heading-one.js +1 -0
  16. package/engine/rules/best-practice/positive-tabindex.js +1 -0
  17. package/engine/rules/best-practice/redundant-alt-phrase.js +1 -0
  18. package/engine/rules/best-practice/redundant-aria-label.js +1 -0
  19. package/engine/rules/best-practice/redundant-aria.js +1 -0
  20. package/engine/rules/best-practice/redundant-image-alt.js +1 -0
  21. package/engine/rules/best-practice/redundant-role.js +1 -0
  22. package/engine/rules/best-practice/redundant-tabindex.js +1 -0
  23. package/engine/rules/best-practice/region.js +1 -0
  24. package/engine/rules/wcag/1.1.1-area-alt.js +1 -0
  25. package/engine/rules/wcag/1.1.1-canvas-alt.js +1 -0
  26. package/engine/rules/wcag/1.1.1-embed-alt.js +1 -0
  27. package/engine/rules/wcag/1.1.1-image-alt.js +1 -0
  28. package/engine/rules/wcag/1.1.1-input-image-alt.js +1 -0
  29. package/engine/rules/wcag/1.1.1-object-alt.js +1 -0
  30. package/engine/rules/wcag/1.1.1-svg-img-alt.js +1 -0
  31. package/engine/rules/wcag/1.2.2-media-captions.js +12 -5
  32. package/engine/rules/wcag/1.3.1-aria-required-children.js +1 -0
  33. package/engine/rules/wcag/1.3.1-aria-required-parent.js +1 -0
  34. package/engine/rules/wcag/1.3.1-definition-list.js +1 -0
  35. package/engine/rules/wcag/1.3.1-dlitem-parent.js +1 -0
  36. package/engine/rules/wcag/1.3.1-form-label.js +1 -0
  37. package/engine/rules/wcag/1.3.1-list-structure.js +1 -0
  38. package/engine/rules/wcag/1.3.1-listitem-parent.js +1 -0
  39. package/engine/rules/wcag/1.3.1-p-as-heading.js +1 -0
  40. package/engine/rules/wcag/1.3.1-table-headers.js +1 -0
  41. package/engine/rules/wcag/1.3.4-orientation.js +1 -0
  42. package/engine/rules/wcag/1.3.5-autocomplete-valid.js +1 -0
  43. package/engine/rules/wcag/1.4.1-link-in-text-block.js +1 -0
  44. package/engine/rules/wcag/1.4.10-reflow.js +1 -0
  45. package/engine/rules/wcag/1.4.11-non-text-contrast.js +1 -0
  46. package/engine/rules/wcag/1.4.12-text-spacing.js +1 -0
  47. package/engine/rules/wcag/1.4.2-audio-control.js +1 -0
  48. package/engine/rules/wcag/1.4.3-color-contrast.js +1 -0
  49. package/engine/rules/wcag/1.4.3-control-contrast.js +1 -0
  50. package/engine/rules/wcag/1.4.4-meta-viewport.js +1 -0
  51. package/engine/rules/wcag/1.4.6-contrast-enhanced.js +1 -0
  52. package/engine/rules/wcag/2.1.1-scrollable-region-focusable.js +1 -0
  53. package/engine/rules/wcag/2.2.1-meta-refresh.js +1 -0
  54. package/engine/rules/wcag/2.2.2-pause-stop-hide.js +1 -0
  55. package/engine/rules/wcag/2.2.2-video-loop-motion.js +1 -0
  56. package/engine/rules/wcag/2.4.1-bypass-blocks.js +1 -0
  57. package/engine/rules/wcag/2.4.11-focus-not-obscured.js +1 -0
  58. package/engine/rules/wcag/2.4.2-document-title.js +1 -0
  59. package/engine/rules/wcag/2.4.3-visual-order-divergence.js +1 -0
  60. package/engine/rules/wcag/2.4.4-link-name.js +1 -0
  61. package/engine/rules/wcag/2.4.4-link-text-generic.js +1 -0
  62. package/engine/rules/wcag/2.4.7-focus-visible.js +1 -0
  63. package/engine/rules/wcag/2.4.9-link-text-generic-only.js +1 -0
  64. package/engine/rules/wcag/2.5.3-label-in-name.js +1 -0
  65. package/engine/rules/wcag/2.5.5-target-size-enhanced.js +1 -0
  66. package/engine/rules/wcag/2.5.8-target-size.js +1 -0
  67. package/engine/rules/wcag/3.1.1-html-lang.js +1 -0
  68. package/engine/rules/wcag/3.1.2-valid-lang-parts.js +1 -0
  69. package/engine/rules/wcag/3.2.2-on-input-navigation.js +1 -0
  70. package/engine/rules/wcag/3.3.1-error-message-linkage.js +1 -0
  71. package/engine/rules/wcag/3.3.8-auth-field-obstruction.js +1 -0
  72. package/engine/rules/wcag/4.1.2-aria-allowed-attr.js +1 -0
  73. package/engine/rules/wcag/4.1.2-aria-attr-valid.js +1 -0
  74. package/engine/rules/wcag/4.1.2-aria-field-name.js +1 -0
  75. package/engine/rules/wcag/4.1.2-aria-hidden-focus.js +1 -0
  76. package/engine/rules/wcag/4.1.2-aria-label-misuse.js +1 -0
  77. package/engine/rules/wcag/4.1.2-aria-valid-refs.js +1 -0
  78. package/engine/rules/wcag/4.1.2-button-name.js +1 -0
  79. package/engine/rules/wcag/4.1.2-composite-widget-name.js +1 -0
  80. package/engine/rules/wcag/4.1.2-dialog-name.js +1 -0
  81. package/engine/rules/wcag/4.1.2-frame-title.js +1 -0
  82. package/engine/rules/wcag/4.1.2-label-for-valid.js +1 -0
  83. package/engine/rules/wcag/4.1.2-nested-interactive.js +1 -0
  84. package/engine/rules/wcag/4.1.2-role-required-aria.js +1 -0
  85. package/engine/rules/wcag/4.1.2-summary-name.js +1 -0
  86. package/engine/rules/wcag/4.1.2-valid-role.js +1 -0
  87. package/package.json +1 -1
  88. package/engine/wcag30-draft.js +0 -149
package/README.md CHANGED
@@ -21,10 +21,6 @@ can verify are returned as a `manualReview` checklist instead of being
21
21
  silently skipped, and findings the engine can't judge conclusively land in
22
22
  `incomplete` rather than being guessed at.
23
23
 
24
- An experimental WCAG 3.0 draft mode (`engine/wcag30-draft.js`) reframes
25
- results against the draft's guidelines — clearly stamped as a draft, because
26
- no tool can honestly claim conformance to an unfinished standard.
27
-
28
24
  ## Getting started
29
25
 
30
26
  ```sh
@@ -39,7 +35,6 @@ const results = await run(document, {
39
35
  // optional: filter rules by tag. Tags are cumulative — WCAG 2.2 AA means
40
36
  // every A/AA rule from 2.0 through 2.2. Omit to run everything.
41
37
  tags: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'],
42
- // standard: 'wcag30-draft', // optional: experimental WCAG 3.0 draft mode
43
38
  });
44
39
 
45
40
  console.log(results.violations, results.passes, results.incomplete);
@@ -89,7 +84,7 @@ static HTML strings. For Node-based testing, drive it through a real browser
89
84
  (`1.1.1-image-alt.js`), plus the catalog index.
90
85
  - `engine/lib/` — shared helpers: DOM traversal, accessible-name
91
86
  computation, contrast math, role model.
92
- - `engine/wcag22.js`, `engine/wcag30-draft.js` — the WCAG catalogs.
87
+ - `engine/wcag22.js` — the WCAG catalog.
93
88
  - `config/project.config.js` — package-local stand-in for the pour
94
89
  monorepo's project config (engine name + version only).
95
90
 
@@ -6,6 +6,6 @@
6
6
  export default {
7
7
  engine: {
8
8
  name: 'pour engine',
9
- version: '1.29.0',
9
+ version: '1.30.0',
10
10
  },
11
11
  };
package/engine/index.js CHANGED
@@ -4,7 +4,6 @@
4
4
  import config from '../config/project.config.js';
5
5
  import rules from './rules/index.js';
6
6
  import wcagCatalog from './wcag22.js';
7
- import wcag30Draft from './wcag30-draft.js';
8
7
  import { isVisible, isRendered, cssPath, htmlSnippet, ownText, collectRoots } from './lib/dom.js';
9
8
  import { accessibleName } from './lib/accessible-name.js';
10
9
  import { resetAuditCaches } from './lib/contrast.js';
@@ -79,16 +78,11 @@ function toResultNode(element, outcome) {
79
78
  /**
80
79
  * Run the audit.
81
80
  * @param {Document|Element} context - what to scan
82
- * @param {{ tags?: string[], exclude?: string, standard?: string, signal?: AbortSignal }} options -
81
+ * @param {{ tags?: string[], exclude?: string, signal?: AbortSignal }} options -
83
82
  * rule selection by tag (wcag2a…wcag22aa, best-practice), empty selects
84
83
  * every rule; exclude is a CSS selector — elements matching it, or inside
85
84
  * a match (including across shadow boundaries), are left out of every
86
- * rule; standard 'wcag30-draft' reframes the report against the WCAG 3.0
87
- * Working Draft (results.standard is stamped draft, the manual checklist
88
- * becomes the draft guidelines, and each rule result carries the draft
89
- * guideline it maps to) — the rules themselves stay WCAG 2.x, which is
90
- * the only honest claim a tool can make about a draft;
91
- * signal aborts the run between rules (throws AbortError)
85
+ * rule; signal aborts the run between rules (throws AbortError)
92
86
  * @param {(progress: object) => void} [onProgress] - called before each rule
93
87
  * starts ({rule, running}), during a rule at each renderer yield with a
94
88
  * fractional done (completed rules + this rule's evaluated-element share,
@@ -97,17 +91,15 @@ function toResultNode(element, outcome) {
97
91
  */
98
92
  export async function run(context = document, options = {}, onProgress) {
99
93
  const auditStarted = performance.now();
100
- const draft30 = options.standard === 'wcag30-draft';
101
94
  const results = {
102
95
  testEngine: { name, version },
103
96
  timestamp: new Date().toISOString(),
104
97
  url: context.location?.href ?? context.ownerDocument?.location?.href ?? '',
105
- standard: draft30 ? wcag30Draft.draftInfo : null,
106
98
  violations: [],
107
99
  passes: [],
108
100
  incomplete: [],
109
101
  inapplicable: [],
110
- manualReview: draft30 ? wcag30Draft.manualReviewChecklist() : manualReviewCriteria(options.tags),
102
+ manualReview: manualReviewCriteria(options.tags),
111
103
  ruleTimings: [],
112
104
  };
113
105
 
@@ -223,17 +215,15 @@ export async function run(context = document, options = {}, onProgress) {
223
215
 
224
216
  const ruleResult = {
225
217
  id: rule.id,
218
+ // Human display name ("Form field labels"); the id stays the
219
+ // machine handle everywhere (selectors, exports, docs anchors).
220
+ name: rule.name ?? rule.id,
226
221
  impact: rule.impact,
227
222
  tags: rule.tags,
228
223
  help: rule.help,
229
224
  description: rule.help,
230
225
  helpUrl: rule.helpUrl,
231
226
  };
232
- if (draft30) {
233
- // Which draft guideline this 2.x rule speaks to (null for rules with
234
- // no draft counterpart, e.g. best-practice extras).
235
- ruleResult.wcag30 = wcag30Draft.guidelineForTags(rule.tags);
236
- }
237
227
  if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
238
228
  if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
239
229
  if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
@@ -252,6 +242,15 @@ export async function run(context = document, options = {}, onProgress) {
252
242
  ms,
253
243
  counts: { ...liveCounts },
254
244
  typeCounts: { ...liveTypeCounts },
245
+ // The completed rule's actual findings, so a UI can BUILD its list
246
+ // as rules finish rather than delivering everything at the end.
247
+ // Present only when the rule found something.
248
+ ...(buckets.fail.length || buckets.incomplete.length ? {
249
+ findings: {
250
+ ...(buckets.fail.length ? { violation: { ...ruleResult, nodes: buckets.fail } } : {}),
251
+ ...(buckets.incomplete.length ? { incomplete: { ...ruleResult, nodes: buckets.incomplete } } : {}),
252
+ },
253
+ } : {}),
255
254
  });
256
255
  }
257
256
 
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'accesskeys',
3
+ name: 'Duplicate access keys',
3
4
  impact: 'serious',
4
5
  tags: ['best-practice'],
5
6
  help: 'The same accesskey must not be assigned twice',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'button-type',
3
+ name: 'Button types in forms',
3
4
  impact: 'minor',
4
5
  tags: ['best-practice'],
5
6
  help: 'Buttons inside forms should declare their type',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'empty-heading',
3
+ name: 'Empty headings',
3
4
  impact: 'minor',
4
5
  tags: ['best-practice'],
5
6
  help: 'Headings must contain text',
@@ -3,6 +3,7 @@
3
3
  import { labelledByName } from '../../lib/accessible-name.js';
4
4
  export default {
5
5
  id: 'fieldset-legend',
6
+ name: 'Grouped field labels',
6
7
  impact: 'moderate',
7
8
  tags: ['best-practice'],
8
9
  help: 'Radio and checkbox groups should be grouped with a label',
@@ -5,6 +5,7 @@ const level = (el) =>
5
5
 
6
6
  export default {
7
7
  id: 'heading-order',
8
+ name: 'Heading order',
8
9
  impact: 'moderate',
9
10
  tags: ['best-practice'],
10
11
  help: 'Headings should step down one level at a time',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'landmark-one-main',
3
+ name: 'One main landmark',
3
4
  impact: 'moderate',
4
5
  tags: ['best-practice'],
5
6
  help: 'The page should have exactly one main landmark',
@@ -22,6 +22,7 @@ function landmarkAncestor(element) {
22
22
 
23
23
  export default {
24
24
  id: 'landmark-top-level',
25
+ name: 'Top-level landmarks',
25
26
  impact: 'moderate',
26
27
  tags: ['best-practice'],
27
28
  help: 'banner, main and contentinfo landmarks must be top level',
@@ -22,6 +22,7 @@ const landmarkName = (element) =>
22
22
 
23
23
  export default {
24
24
  id: 'landmark-unique',
25
+ name: 'Distinguishable landmarks',
25
26
  impact: 'moderate',
26
27
  tags: ['best-practice'],
27
28
  help: 'Landmarks of the same type need distinguishing labels',
@@ -5,6 +5,7 @@
5
5
  // pair usually reads better as label + aria-describedby.
6
6
  export default {
7
7
  id: 'multiple-labels',
8
+ name: 'Multiple labels',
8
9
  impact: 'minor',
9
10
  tags: ['best-practice'],
10
11
  help: 'Form fields should not have multiple labels',
@@ -4,6 +4,7 @@ const MENTIONS_NEW_WINDOW = /new (window|tab)|opens? in/i;
4
4
 
5
5
  export default {
6
6
  id: 'new-window-link',
7
+ name: 'New-window warnings',
7
8
  impact: 'minor',
8
9
  tags: ['best-practice'],
9
10
  help: 'Links opening a new window should say so',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'no-autofocus',
3
+ name: 'Autofocus on load',
3
4
  impact: 'minor',
4
5
  tags: ['best-practice'],
5
6
  help: 'autofocus disorients assistive-technology users',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'page-heading-one',
3
+ name: 'Leading page heading',
3
4
  impact: 'moderate',
4
5
  tags: ['best-practice'],
5
6
  help: 'Every page should start its outline with an <h1>',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'positive-tabindex',
3
+ name: 'Positive tabindex',
3
4
  impact: 'serious',
4
5
  tags: ['best-practice'],
5
6
  help: 'Positive tabindex values disrupt the natural focus order',
@@ -5,6 +5,7 @@ const REDUNDANT_PHRASE = /^(image|picture|graphic|icon)\b( of\b)?/i;
5
5
 
6
6
  export default {
7
7
  id: 'redundant-alt-phrase',
8
+ name: 'Redundant alt phrasing',
8
9
  impact: 'minor',
9
10
  tags: ['best-practice'],
10
11
  help: 'alt text should not start with "image of" or similar',
@@ -11,6 +11,7 @@ const NAME_FROM_CONTENT = new Set(['button', 'link', 'menuitem', 'menuitemcheckb
11
11
 
12
12
  export default {
13
13
  id: 'redundant-aria-label',
14
+ name: 'Redundant aria-label',
14
15
  impact: 'minor',
15
16
  tags: ['best-practice'],
16
17
  help: 'aria-label should not repeat the visible text',
@@ -10,6 +10,7 @@ const NATIVE_PAIRS = [
10
10
 
11
11
  export default {
12
12
  id: 'redundant-aria',
13
+ name: 'Redundant ARIA attributes',
13
14
  impact: 'minor',
14
15
  tags: ['best-practice'],
15
16
  help: 'aria attributes should not duplicate native HTML attributes',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'redundant-image-alt',
3
+ name: 'Redundant image alt',
3
4
  impact: 'minor',
4
5
  tags: ['best-practice'],
5
6
  help: 'Image alt should not repeat adjacent link or button text',
@@ -28,6 +28,7 @@ function implicitRole(element) {
28
28
 
29
29
  export default {
30
30
  id: 'redundant-role',
31
+ name: 'Redundant roles',
31
32
  impact: 'minor',
32
33
  tags: ['best-practice'],
33
34
  help: 'role attributes should not repeat the element’s built-in role',
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  id: 'redundant-tabindex',
3
+ name: 'Redundant tabindex',
3
4
  impact: 'minor',
4
5
  tags: ['best-practice'],
5
6
  help: 'tabindex="0" is unnecessary on natively focusable elements',
@@ -6,6 +6,7 @@ const LANDMARK =
6
6
 
7
7
  export default {
8
8
  id: 'region',
9
+ name: 'Content outside landmarks',
9
10
  impact: 'moderate',
10
11
  tags: ['best-practice'],
11
12
  help: 'All readable content belongs inside a landmark region',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.1.1 Non-text Content (Level A) · SC 2.4.4 Link Purpose (In Context) (Level A)
2
2
  export default {
3
3
  id: 'area-alt',
4
+ name: 'Image map alt text',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag111', 'wcag244'],
6
7
  help: 'Image map areas must have alternative text',
@@ -15,6 +15,7 @@
15
15
  // matters.
16
16
  export default {
17
17
  id: 'canvas-alt',
18
+ name: 'Canvas accessible name',
18
19
  impact: 'moderate',
19
20
  tags: ['wcag2a', 'wcag111'],
20
21
  help: 'Meaningful <canvas> graphics need an accessible name or fallback',
@@ -11,6 +11,7 @@
11
11
  // is, but no criterion provably fails.
12
12
  export default {
13
13
  id: 'embed-alt',
14
+ name: 'Embed accessible name',
14
15
  impact: 'serious',
15
16
  tags: ['wcag2a', 'wcag111'],
16
17
  help: '<embed> needs an accessible name',
@@ -10,6 +10,7 @@ const GENERIC_ALT = /^(image|img|photo|photograph|picture|graphic|icon|untitled|
10
10
 
11
11
  export default {
12
12
  id: 'image-alt',
13
+ name: 'Image alt text',
13
14
  impact: 'critical',
14
15
  tags: ['wcag2a', 'wcag111'],
15
16
  help: 'Every <img> needs a text alternative',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.1.1 Non-text Content (Level A) · SC 4.1.2 Name, Role, Value (Level A)
2
2
  export default {
3
3
  id: 'input-image-alt',
4
+ name: 'Image button alt text',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag111', 'wcag412'],
6
7
  help: 'Image inputs need alt text describing their action',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.1.1 Non-text Content (Level A)
2
2
  export default {
3
3
  id: 'object-alt',
4
+ name: 'Object alt text',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag111'],
6
7
  help: '<object> embeds must have a text alternative',
@@ -3,6 +3,7 @@ import { labelledByName } from '../../lib/accessible-name.js';
3
3
 
4
4
  export default {
5
5
  id: 'svg-img-alt',
6
+ name: 'SVG accessible name',
6
7
  impact: 'serious',
7
8
  tags: ['wcag2a', 'wcag111'],
8
9
  help: 'Inline SVG and role="img" graphics need an accessible name',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.2.2 Captions (Prerecorded) (Level A)
2
2
  export default {
3
3
  id: 'media-captions',
4
+ name: 'Video captions',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag122'],
6
7
  help: 'Video content must have captions',
@@ -21,17 +22,23 @@ export default {
21
22
  if (!element.currentSrc && !element.getAttribute('src') && !element.querySelector('source')) {
22
23
  return { status: 'pass' };
23
24
  }
24
- // The decorative no-audio-path signature — the same boundary
25
- // video-loop-motion draws for 2.2.2: muted, autoplaying, no controls.
26
- // With no controls there is no user path to unmute, so whatever audio
27
- // the file may carry is never presented to anyone; audio that cannot be
25
+ // The decorative no-audio-path signature: muted with no controls. With
26
+ // no controls there is no user path to unmute, so whatever audio the
27
+ // file may carry is never presented to anyone; audio that cannot be
28
28
  // presented is not audio content under 1.2.2, and asking a human to
29
29
  // verify captions for it is noise (measured on the benchmark corpus:
30
30
  // our own site's aria-hidden hero loops, ffprobe-confirmed to carry no
31
31
  // audio stream, plus the same family on four news/university homes).
32
+ // HOW playback starts is deliberately not part of the signature: an
33
+ // earlier autoplay condition missed the two other members of the same
34
+ // family, measured on our own site — a crossfade B-roll take that only
35
+ // ever plays via script (no autoplay attribute, so the review fired
36
+ // exactly when the script had wired its src), and heroes whose
37
+ // autoplay attribute a reduced-motion handler removes. Muteness and
38
+ // controls decide whether audio can reach anyone; autoplay never did.
32
39
  // Properties, not attributes: script-muted heroes count too. A muted
33
40
  // video WITH controls keeps its review — one click unmutes it.
34
- if (element.muted && element.autoplay && !element.controls) {
41
+ if (element.muted && !element.controls) {
35
42
  return { status: 'pass' };
36
43
  }
37
44
  // Captions may be burned in or provided by the player — a human must check.
@@ -47,6 +47,7 @@ function candidateDescendants(element) {
47
47
 
48
48
  export default {
49
49
  id: 'aria-required-children',
50
+ name: 'Required ARIA children',
50
51
  impact: 'critical',
51
52
  tags: ['wcag2a', 'wcag131'],
52
53
  help: 'Composite ARIA roles must contain their required children',
@@ -20,6 +20,7 @@ const IMPLICIT_CONTAINER = { ul: 'list', ol: 'list', menu: 'list', table: 'table
20
20
 
21
21
  export default {
22
22
  id: 'aria-required-parent',
23
+ name: 'Required ARIA parent',
23
24
  impact: 'critical',
24
25
  tags: ['wcag2a', 'wcag131'],
25
26
  help: 'ARIA child roles must be inside their required container role',
@@ -3,6 +3,7 @@ const ALLOWED = new Set(['DT', 'DD', 'DIV', 'SCRIPT', 'TEMPLATE']);
3
3
 
4
4
  export default {
5
5
  id: 'definition-list',
6
+ name: 'Definition list structure',
6
7
  impact: 'serious',
7
8
  tags: ['wcag2a', 'wcag131'],
8
9
  help: '<dl> must be structured as term/description pairs',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.3.1 Info and Relationships (Level A)
2
2
  export default {
3
3
  id: 'dlitem-parent',
4
+ name: 'Definition item placement',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag131'],
6
7
  help: '<dt> and <dd> must be inside a <dl>',
@@ -6,6 +6,7 @@ import { labelledByName } from '../../lib/accessible-name.js';
6
6
 
7
7
  export default {
8
8
  id: 'form-label',
9
+ name: 'Form field labels',
9
10
  impact: 'critical',
10
11
  tags: ['wcag2a', 'wcag131', 'wcag332', 'wcag412'],
11
12
  help: 'Every form field needs a label',
@@ -8,6 +8,7 @@ const ITEM_ROLES = ['listitem', 'presentation', 'none'];
8
8
 
9
9
  export default {
10
10
  id: 'list-structure',
11
+ name: 'List structure',
11
12
  impact: 'serious',
12
13
  tags: ['wcag2a', 'wcag131'],
13
14
  help: 'Lists must only contain list items',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.3.1 Info and Relationships (Level A)
2
2
  export default {
3
3
  id: 'listitem-parent',
4
+ name: 'List item placement',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag131'],
6
7
  help: '<li> must be inside a list',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.3.1 Info and Relationships (Level A)
2
2
  export default {
3
3
  id: 'p-as-heading',
4
+ name: 'Styled text as heading',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag131'],
6
7
  help: 'Bold paragraphs should not stand in for headings',
@@ -9,6 +9,7 @@
9
9
  // better than false positives on real pages.
10
10
  export default {
11
11
  id: 'table-headers',
12
+ name: 'Table header associations',
12
13
  impact: 'serious',
13
14
  tags: ['wcag2a', 'wcag131'],
14
15
  help: 'Data tables must have properly associated header cells',
@@ -46,6 +46,7 @@ function scanRules(rules, insideOrientation, findings) {
46
46
 
47
47
  export default {
48
48
  id: 'orientation-lock',
49
+ name: 'Orientation lock',
49
50
  impact: 'serious',
50
51
  tags: ['wcag21aa', 'wcag134'],
51
52
  help: 'Content must work in both portrait and landscape',
@@ -16,6 +16,7 @@ const TOKENS = new Set([
16
16
 
17
17
  export default {
18
18
  id: 'autocomplete-valid',
19
+ name: 'Autocomplete tokens',
19
20
  impact: 'serious',
20
21
  tags: ['wcag21aa', 'wcag135'],
21
22
  help: 'autocomplete attributes must use valid tokens',
@@ -3,6 +3,7 @@ import { parseColor, contrastRatio } from '../../lib/contrast.js';
3
3
 
4
4
  export default {
5
5
  id: 'link-in-text-block',
6
+ name: 'Link distinction in text',
6
7
  impact: 'serious',
7
8
  tags: ['wcag2a', 'wcag141'],
8
9
  help: 'Links inside text must be distinguishable by more than colour',
@@ -6,6 +6,7 @@
6
6
  // and images are exempt content the geometry can't distinguish.
7
7
  export default {
8
8
  id: 'reflow',
9
+ name: 'Content reflow',
9
10
  impact: 'moderate',
10
11
  tags: ['wcag21aa', 'wcag1410'],
11
12
  help: 'Content should reflow to one-dimensional scrolling',
@@ -10,6 +10,7 @@ import { parseColor, contrastRatio, composite, effectiveBackground, showRatio }
10
10
 
11
11
  export default {
12
12
  id: 'non-text-contrast',
13
+ name: 'Control & icon contrast',
13
14
  impact: 'serious',
14
15
  tags: ['wcag21aa', 'wcag1411'],
15
16
  help: 'Form fields and icons need a 3:1 visible boundary',
@@ -16,6 +16,7 @@ const clipped = (element) =>
16
16
 
17
17
  export default {
18
18
  id: 'text-spacing',
19
+ name: 'Text spacing overrides',
19
20
  impact: 'serious',
20
21
  tags: ['wcag21aa', 'wcag1412'],
21
22
  help: 'Text must survive user spacing overrides without being cut off',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 1.4.2 Audio Control (Level A)
2
2
  export default {
3
3
  id: 'audio-control',
4
+ name: 'Auto-playing audio control',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag142'],
6
7
  help: 'Auto-playing audio must be stoppable or muted',
@@ -916,6 +916,7 @@ export function createContrastRule({ id, tags, help, helpUrl, thresholds }) {
916
916
 
917
917
  export default createContrastRule({
918
918
  id: 'color-contrast',
919
+ name: 'Text contrast',
919
920
  tags: ['wcag2aa', 'wcag143'],
920
921
  help: 'Text must have sufficient contrast against its background',
921
922
  helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html',
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  export default {
11
11
  id: 'control-contrast',
12
+ name: 'Form control contrast',
12
13
  impact: 'serious',
13
14
  tags: ['wcag2aa', 'wcag143'],
14
15
  help: 'Text inside form controls must have sufficient contrast',
@@ -3,6 +3,7 @@ import { isEmbeddedDocument } from '../../lib/dom.js';
3
3
 
4
4
  export default {
5
5
  id: 'meta-viewport',
6
+ name: 'Viewport zoom lock',
6
7
  impact: 'critical',
7
8
  tags: ['wcag2aa', 'wcag144'],
8
9
  help: 'The viewport must let users zoom to at least 200%',
@@ -4,6 +4,7 @@ import { createContrastRule } from './1.4.3-color-contrast.js';
4
4
 
5
5
  export default createContrastRule({
6
6
  id: 'color-contrast-enhanced',
7
+ name: 'Enhanced text contrast',
7
8
  tags: ['wcag2aaa', 'wcag146'],
8
9
  help: 'Text must have enhanced contrast against its background (AAA)',
9
10
  helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced.html',
@@ -43,6 +43,7 @@ function hasTabbableContent(node) {
43
43
 
44
44
  export default {
45
45
  id: 'scrollable-region-focusable',
46
+ name: 'Keyboard-reachable scroll areas',
46
47
  impact: 'serious',
47
48
  tags: ['wcag2a', 'wcag211', 'wcag213'], // 2.1.3 is 2.1.1's AAA twin — same check satisfies both
48
49
  help: 'Scrollable regions must be reachable by keyboard',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 2.2.1 Timing Adjustable (Level A)
2
2
  export default {
3
3
  id: 'meta-refresh',
4
+ name: 'Timed page refresh',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag221'],
6
7
  help: 'The page must not use a timed refresh',
@@ -79,6 +79,7 @@ function endlesslyMoving(doc) {
79
79
 
80
80
  export default {
81
81
  id: 'pause-stop-hide',
82
+ name: 'Pausable moving content',
82
83
  impact: 'serious',
83
84
  tags: ['wcag2a', 'wcag222'],
84
85
  help: 'Moving content must be pausable',
@@ -15,6 +15,7 @@
15
15
  // abstaining, not clearing.
16
16
  export default {
17
17
  id: 'video-loop-motion',
18
+ name: 'Looping video control',
18
19
  impact: 'serious',
19
20
  tags: ['wcag2a', 'wcag222'],
20
21
  help: 'Looping autoplay video must be pausable',
@@ -3,6 +3,7 @@ import { collectRoots, isEmbeddedDocument } from '../../lib/dom.js';
3
3
 
4
4
  export default {
5
5
  id: 'bypass-blocks',
6
+ name: 'Skip to content',
6
7
  impact: 'serious',
7
8
  tags: ['wcag2a', 'wcag241'],
8
9
  help: 'The page must offer a way to skip repeated blocks',
@@ -31,6 +31,7 @@ function edgeReserved(doc, edge, needed) {
31
31
 
32
32
  export default {
33
33
  id: 'focus-not-obscured',
34
+ name: 'Unobscured focus',
34
35
  impact: 'serious',
35
36
  tags: ['wcag22aa', 'wcag2411'],
36
37
  help: 'Focused elements must not be fully hidden behind overlays',
@@ -32,6 +32,7 @@ const UNDESCRIPTIVE = new Set([
32
32
 
33
33
  export default {
34
34
  id: 'document-title',
35
+ name: 'Page title',
35
36
  impact: 'serious',
36
37
  tags: ['wcag2a', 'wcag242'],
37
38
  help: 'The page must have a title that describes its topic or purpose',
@@ -48,6 +48,7 @@ function visualSequence(entries, rightToLeft) {
48
48
 
49
49
  export default {
50
50
  id: 'visual-order-divergence',
51
+ name: 'Focus order vs visual order',
51
52
  impact: 'moderate',
52
53
  tags: ['wcag2a', 'wcag243'],
53
54
  help: 'CSS reordering should not make Tab jump against the visual order',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 2.4.4 Link Purpose (In Context) (Level A) · SC 4.1.2 Name, Role, Value (Level A)
2
2
  export default {
3
3
  id: 'link-name',
4
+ name: 'Link names',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag244', 'wcag412'],
6
7
  help: 'Links must have an accessible name',
@@ -78,6 +78,7 @@ export function createLinkPurposeRule({ id, impact, tags, help, helpUrl, verdict
78
78
 
79
79
  export default createLinkPurposeRule({
80
80
  id: 'link-text-generic',
81
+ name: 'Generic link text',
81
82
  impact: 'moderate',
82
83
  tags: ['wcag2a', 'wcag244'],
83
84
  help: 'Link text should describe where the link goes',
@@ -52,6 +52,7 @@ function focusSuppressors(doc) {
52
52
 
53
53
  export default {
54
54
  id: 'focus-visible',
55
+ name: 'Visible focus',
55
56
  impact: 'serious',
56
57
  tags: ['wcag2aa', 'wcag247'],
57
58
  help: 'Keyboard focus must remain visible',
@@ -14,6 +14,7 @@ import { createLinkPurposeRule } from './2.4.4-link-text-generic.js';
14
14
 
15
15
  export default createLinkPurposeRule({
16
16
  id: 'link-text-generic-only',
17
+ name: 'Self-sufficient link text',
17
18
  impact: 'moderate',
18
19
  tags: ['wcag2aaa', 'wcag249'],
19
20
  help: 'Link text alone must identify where the link goes (AAA)',
@@ -71,6 +71,7 @@ function visibleText(nodes) {
71
71
 
72
72
  export default {
73
73
  id: 'label-in-name',
74
+ name: 'Label in accessible name',
74
75
  impact: 'serious',
75
76
  tags: ['wcag21a', 'wcag253'],
76
77
  help: 'The visible label must be part of the accessible name',
@@ -6,6 +6,7 @@ import { createTargetSizeRule } from './2.5.8-target-size.js';
6
6
 
7
7
  export default createTargetSizeRule({
8
8
  id: 'target-size-enhanced',
9
+ name: 'Enhanced target size',
9
10
  tags: ['wcag2aaa', 'wcag255'],
10
11
  help: 'Interactive targets must be at least 44×44 pixels (AAA)',
11
12
  helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html',
@@ -467,6 +467,7 @@ export function createTargetSizeRule({ id, tags, help, helpUrl, min, spacingExce
467
467
 
468
468
  export default createTargetSizeRule({
469
469
  id: 'target-size',
470
+ name: 'Target size',
470
471
  tags: ['wcag22aa', 'wcag258'],
471
472
  help: 'Interactive targets must be at least 24×24 pixels',
472
473
  helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html',
@@ -12,6 +12,7 @@ const LANG_PATTERN = /^([a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*|[xXiI](-[a-zA-Z0-9]{1,
12
12
 
13
13
  export default {
14
14
  id: 'html-lang',
15
+ name: 'Page language',
15
16
  impact: 'serious',
16
17
  tags: ['wcag2a', 'wcag311'],
17
18
  help: 'The <html> element must declare a valid language',
@@ -6,6 +6,7 @@ const LANG_PATTERN = /^([a-zA-Z]{2,3}(-[a-zA-Z0-9]{1,8})*|[xXiI](-[a-zA-Z0-9]{1,
6
6
 
7
7
  export default {
8
8
  id: 'valid-lang-parts',
9
+ name: 'Part language tags',
9
10
  impact: 'serious',
10
11
  tags: ['wcag2aa', 'wcag312'],
11
12
  help: 'lang attributes on page parts must be valid',
@@ -20,6 +20,7 @@ const NAVIGATES = /\blocation\s*(=|\.\s*(href|assign|replace|reload))|\.(submit|
20
20
 
21
21
  export default {
22
22
  id: 'on-input-navigation',
23
+ name: 'Unexpected context change',
23
24
  impact: 'serious',
24
25
  tags: ['wcag2a', 'wcag322', 'wcag321'],
25
26
  help: 'Changing or focusing a control must not unexpectedly change context',
@@ -25,6 +25,7 @@
25
25
  // value except false/undefined — "spelling" and "grammar" count.
26
26
  export default {
27
27
  id: 'error-message-linkage',
28
+ name: 'Error message linkage',
28
29
  impact: 'serious',
29
30
  tags: ['wcag2a', 'wcag331'],
30
31
  help: 'Fields marked invalid need their error described in text',
@@ -13,6 +13,7 @@
13
13
  // case worth asserting.
14
14
  export default {
15
15
  id: 'auth-field-obstruction',
16
+ name: 'Accessible login fields',
16
17
  impact: 'serious',
17
18
  tags: ['wcag22aa', 'wcag338'],
18
19
  help: 'Login fields must not block paste or password managers',
@@ -39,6 +39,7 @@ const HANDLED_ELSEWHERE = new Set(['label', 'labelledby']);
39
39
 
40
40
  export default {
41
41
  id: 'aria-allowed-attr',
42
+ name: 'Allowed ARIA attributes',
42
43
  impact: 'moderate',
43
44
  tags: ['wcag2a', 'wcag412'],
44
45
  help: 'ARIA attributes must be supported by the element’s role',
@@ -24,6 +24,7 @@ const ENUMS = {
24
24
 
25
25
  export default {
26
26
  id: 'aria-attr-valid',
27
+ name: 'Valid ARIA values',
27
28
  impact: 'critical',
28
29
  tags: ['wcag2a', 'wcag412'],
29
30
  help: 'aria attributes must exist and have valid values',
@@ -23,6 +23,7 @@ function authorName(element) {
23
23
 
24
24
  export default {
25
25
  id: 'aria-field-name',
26
+ name: 'ARIA field names',
26
27
  impact: 'serious',
27
28
  tags: ['wcag2a', 'wcag412'],
28
29
  help: 'ARIA fields and value widgets must have an accessible name',
@@ -6,6 +6,7 @@ const FOCUSABLE = 'a[href], area[href], button, input, select, textarea, summary
6
6
 
7
7
  export default {
8
8
  id: 'aria-hidden-focus',
9
+ name: 'Focusable hidden content',
9
10
  impact: 'serious',
10
11
  tags: ['wcag2a', 'wcag412'],
11
12
  help: 'Nothing focusable may sit inside aria-hidden="true"',
@@ -42,6 +42,7 @@ const NAME_FROM_CONTENT = 'a[href], button, summary, h1, h2, h3, h4, h5, h6, th,
42
42
 
43
43
  export default {
44
44
  id: 'aria-label-misuse',
45
+ name: 'aria-label placement',
45
46
  impact: 'moderate',
46
47
  tags: ['wcag2a', 'wcag412'],
47
48
  help: 'aria-label on a plain container is prohibited by ARIA, so its announcement is not guaranteed',
@@ -107,6 +107,7 @@ function outcome({ missing, ambiguous }, probed, element, accessibleName) {
107
107
 
108
108
  export default {
109
109
  id: 'aria-valid-refs',
110
+ name: 'ARIA id references',
110
111
  // Moderate, not critical: with 4.1.1 Parsing retired in WCAG 2.2 these
111
112
  // are 4.1.2 name/state defects, and aria-controls in particular has weak
112
113
  // assistive-technology support — real, but rarely blocking.
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 4.1.2 Name, Role, Value (Level A)
2
2
  export default {
3
3
  id: 'button-name',
4
+ name: 'Button names',
4
5
  impact: 'critical',
5
6
  tags: ['wcag2a', 'wcag412'],
6
7
  help: 'Buttons must have an accessible name',
@@ -10,6 +10,7 @@
10
10
  // where the binary, assertable failure lives.
11
11
  export default {
12
12
  id: 'composite-widget-name',
13
+ name: 'Widget item names',
13
14
  impact: 'serious',
14
15
  tags: ['wcag2a', 'wcag412'],
15
16
  help: 'Tabs, menu items, options and tree items need an accessible name',
@@ -8,6 +8,7 @@ import { labelledByName } from '../../lib/accessible-name.js';
8
8
 
9
9
  export default {
10
10
  id: 'dialog-name',
11
+ name: 'Dialog names',
11
12
  impact: 'serious',
12
13
  tags: ['wcag2a', 'wcag412'],
13
14
  help: 'Dialogs must have an accessible name',
@@ -1,6 +1,7 @@
1
1
  // WCAG SC 4.1.2 Name, Role, Value (Level A)
2
2
  export default {
3
3
  id: 'frame-title',
4
+ name: 'Frame titles',
4
5
  impact: 'serious',
5
6
  tags: ['wcag2a', 'wcag412'],
6
7
  help: 'Frames must have a title describing their content',
@@ -3,6 +3,7 @@ const LABELABLE = 'input, select, textarea, button, meter, output, progress';
3
3
 
4
4
  export default {
5
5
  id: 'label-for-valid',
6
+ name: 'Label target references',
6
7
  impact: 'serious',
7
8
  tags: ['wcag2a', 'wcag412'],
8
9
  help: 'label[for] must reference a form control that exists',
@@ -8,6 +8,7 @@ const INTERACTIVE =
8
8
 
9
9
  export default {
10
10
  id: 'nested-interactive',
11
+ name: 'Nested controls',
11
12
  impact: 'serious',
12
13
  tags: ['wcag2a', 'wcag412'],
13
14
  help: 'A control must not contain another control',
@@ -11,6 +11,7 @@ const REQUIRED = {
11
11
 
12
12
  export default {
13
13
  id: 'role-required-aria',
14
+ name: 'Required role states',
14
15
  impact: 'serious',
15
16
  tags: ['wcag2a', 'wcag412'],
16
17
  help: 'Some roles require a state attribute to work',
@@ -11,6 +11,7 @@
11
11
  // label ("Details").
12
12
  export default {
13
13
  id: 'summary-name',
14
+ name: 'Disclosure names',
14
15
  impact: 'serious',
15
16
  tags: ['wcag2a', 'wcag412'],
16
17
  help: 'Disclosure controls (<summary>) need an accessible name',
@@ -24,6 +24,7 @@ const VALID_ROLES = new Set([
24
24
 
25
25
  export default {
26
26
  id: 'valid-role',
27
+ name: 'Valid roles',
27
28
  impact: 'critical',
28
29
  tags: ['wcag2a', 'wcag412'],
29
30
  help: 'role attributes must use valid ARIA roles',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pour-engine",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "Accessibility (WCAG 2.2) audit engine written from the W3C specifications — runs entirely in the browser, no dependencies.",
5
5
  "type": "module",
6
6
  "main": "engine/index.js",
@@ -1,149 +0,0 @@
1
- // The WCAG 3.0 guideline catalog — derived from the W3C Working DRAFT of
2
- // 3 March 2026 (https://www.w3.org/TR/wcag-3.0/). WCAG 3.0 is a work in
3
- // progress: the W3C says it "may be updated, replaced, or obsoleted" and it
4
- // is not appropriate for conformance claims. Everything this module exports
5
- // is therefore marked draft, and every UI that shows it must say so.
6
- //
7
- // The draft has no A/AA/AAA levels and no success criteria; it organizes
8
- // outcome-style guidelines (with core/supplemental requirements below them)
9
- // under twelve functional groups. Our automated rules are written against
10
- // WCAG 2.x; `scToGuideline` maps each 2.x criterion we automate to the
11
- // draft guideline that covers the same user need, judged by reading both
12
- // specs side by side. Revisit the mapping with every new draft.
13
-
14
- export const draftInfo = {
15
- id: 'wcag30-draft',
16
- name: 'WCAG 3.0',
17
- status: 'W3C Working Draft',
18
- dated: '2026-03-03',
19
- draft: true,
20
- url: 'https://www.w3.org/TR/wcag-3.0/',
21
- note: 'A draft under active development: findings are WCAG 2.x rule results mapped to draft guidelines. Not suitable for conformance claims.',
22
- };
23
-
24
- const g = (num, name, group, anchor) => ({
25
- num,
26
- name,
27
- group,
28
- url: `https://www.w3.org/TR/wcag-3.0/#${anchor}`,
29
- });
30
-
31
- // All 44 guidelines of the 2026-03-03 draft, in document order.
32
- export const guidelines = [
33
- // 2.1 Images and media
34
- g('2.1.1', 'Image alternatives', 'Images and media', 'image-alternatives'),
35
- g('2.1.2', 'Media alternatives', 'Images and media', 'media-alternatives'),
36
- g('2.1.3', 'Non-text alternatives', 'Images and media', 'non-text-alternatives'),
37
- g('2.1.4', 'Captions', 'Images and media', 'captions'),
38
- g('2.1.5', 'Audio descriptions', 'Images and media', 'audio-descriptions'),
39
- g('2.1.6', 'Figure captions', 'Images and media', 'figure-captions'),
40
- g('2.1.7', 'Single sense', 'Images and media', 'single-sense'),
41
- // 2.2 Text and wording
42
- g('2.2.1', 'Text appearance', 'Text and wording', 'text-appearance'),
43
- g('2.2.2', 'Text-to-speech', 'Text and wording', 'text-to-speech'),
44
- g('2.2.3', 'Clear language', 'Text and wording', 'clear-language'),
45
- // 2.3 Interactive components
46
- g('2.3.1', 'Keyboard focus appearance', 'Interactive components', 'keyboard-focus-appearance'),
47
- g('2.3.2', 'Pointer focus appearance', 'Interactive components', 'pointer-focus-appearance'),
48
- g('2.3.3', 'Navigating content', 'Interactive components', 'navigating-content'),
49
- g('2.3.4', 'Expected behavior', 'Interactive components', 'expected-behavior'),
50
- g('2.3.5', 'Control information', 'Interactive components', 'control-information'),
51
- // 2.4 Input / operation
52
- g('2.4.1', 'Keyboard interface input', 'Input and operation', 'keyboard-interface-input'),
53
- g('2.4.2', 'Physical or cognitive effort when using keyboard', 'Input and operation', 'physical-or-cognitive-effort-when-using-keyboard'),
54
- g('2.4.3', 'Pointer input', 'Input and operation', 'pointer-input'),
55
- g('2.4.4', 'Speech and voice input', 'Input and operation', 'speech-and-voice-input'),
56
- g('2.4.5', 'Input operation', 'Input and operation', 'input-operation-0'),
57
- g('2.4.6', 'Authentication', 'Input and operation', 'authentication'),
58
- // 2.5 Error handling
59
- g('2.5.1', 'Correct errors', 'Error handling', 'correct-errors'),
60
- g('2.5.2', 'Prevent errors', 'Error handling', 'prevent-errors'),
61
- // 2.6 Animation and movement
62
- g('2.6.1', 'Avoid physical harm', 'Animation and movement', 'avoid-physical-harm'),
63
- // 2.7 Layout
64
- g('2.7.1', 'Recognizable layouts', 'Layout', 'recognizable-layouts'),
65
- g('2.7.2', 'User orientation', 'Layout', 'user-orientation'),
66
- g('2.7.3', 'Structure', 'Layout', 'structure'),
67
- g('2.7.4', 'No obstruction', 'Layout', 'no-obstruction'),
68
- // 2.8 Consistency across views
69
- g('2.8.1', 'Consistency', 'Consistency across views', 'consistency'),
70
- // 2.9 Process and task completion
71
- g('2.9.1', 'Avoid exclusionary cognitive tasks', 'Process and task completion', 'avoid-exclusionary-cognitive-tasks'),
72
- g('2.9.2', 'Adequate time', 'Process and task completion', 'adequate-time'),
73
- g('2.9.3', 'Avoid deception', 'Process and task completion', 'avoid-deception'),
74
- g('2.9.4', 'Retain information', 'Process and task completion', 'retain-information'),
75
- g('2.9.5', 'Complete tasks', 'Process and task completion', 'complete-tasks'),
76
- g('2.9.6', 'Unnecessary steps', 'Process and task completion', 'unnecessary-steps'),
77
- // 2.10 Policy and protection
78
- g('2.10.1', 'Risk', 'Policy and protection', 'risk'),
79
- g('2.10.2', 'Algorithms', 'Policy and protection', 'algorithms'),
80
- // 2.11 Help and feedback
81
- g('2.11.1', 'Help available', 'Help and feedback', 'help-available'),
82
- g('2.11.2', 'Feedback', 'Help and feedback', 'feedback'),
83
- // 2.12 User control
84
- g('2.12.1', 'Assistive technology control', 'User control', 'assistive-technology-control'),
85
- g('2.12.2', 'Control text', 'User control', 'control-text'),
86
- g('2.12.3', 'Adjustable viewport', 'User control', 'adjustable-viewport'),
87
- g('2.12.4', 'Media control', 'User control', 'media-control'),
88
- g('2.12.5', 'Content changes', 'User control', 'content-changes'),
89
- ];
90
-
91
- // WCAG 2.x criterion (as tagged on rules: wcag111 = SC 1.1.1) → the draft
92
- // guideline covering the same user need. Only criteria with automated rules
93
- // appear; the judgment is ours, made by reading both documents.
94
- export const scToGuideline = {
95
- 111: '2.1.1', // Non-text Content → Image alternatives
96
- 122: '2.1.4', // Captions (Prerecorded) → Captions
97
- 131: '2.7.3', // Info and Relationships → Structure
98
- 135: '2.4.5', // Identify Input Purpose → Input operation
99
- 141: '2.2.1', // Use of Color (links in text) → Text appearance
100
- 142: '2.12.4', // Audio Control → Media control
101
- 143: '2.2.1', // Contrast (Minimum) → Text appearance
102
- 144: '2.12.3', // Resize Text (viewport zoom) → Adjustable viewport
103
- 146: '2.2.1', // Contrast (Enhanced) → Text appearance
104
- 211: '2.4.1', // Keyboard → Keyboard interface input
105
- 221: '2.9.2', // Timing Adjustable → Adequate time
106
- 222: '2.12.4', // Pause, Stop, Hide → Media control
107
- 241: '2.3.3', // Bypass Blocks → Navigating content
108
- 242: '2.7.2', // Page Titled → User orientation
109
- 244: '2.3.5', // Link Purpose (In Context) → Control information
110
- 255: '2.4.3', // Target Size (Enhanced) → Pointer input
111
- 258: '2.4.3', // Target Size (Minimum) → Pointer input
112
- 311: '2.2.2', // Language of Page → Text-to-speech
113
- 312: '2.2.2', // Language of Parts → Text-to-speech
114
- 332: '2.3.5', // Labels or Instructions → Control information
115
- 412: '2.12.1', // Name, Role, Value → Assistive technology control
116
- };
117
-
118
- const guidelineByNum = new Map(guidelines.map((entry) => [entry.num, entry]));
119
- const partlyAutomated = new Set(Object.values(scToGuideline));
120
-
121
- /** The draft guideline (num/name/group/url) a rule's wcagNNN tag maps to. */
122
- export function guidelineForTags(tags) {
123
- for (const tag of tags ?? []) {
124
- const sc = tag.match(/^wcag(\d{3,4})$/)?.[1];
125
- if (sc && scToGuideline[sc]) return guidelineByNum.get(scToGuideline[sc]);
126
- }
127
- return null;
128
- }
129
-
130
- /**
131
- * The whole draft as a review checklist. Every guideline is listed — the
132
- * draft is too young for any of it to be silently skipped — shaped like the
133
- * 2.2 manual-review entries so existing UIs render it unchanged: `level` has
134
- * no meaning in the draft (no conformance levels yet) and `principle`
135
- * carries the draft's functional group.
136
- */
137
- export function manualReviewChecklist() {
138
- return guidelines.map(({ num, name, group, url }) => ({
139
- num,
140
- name,
141
- level: 'draft',
142
- principle: group,
143
- url,
144
- automation: partlyAutomated.has(num) ? 'partial' : 'manual',
145
- draft: true,
146
- }));
147
- }
148
-
149
- export default { draftInfo, guidelines, scToGuideline, guidelineForTags, manualReviewChecklist };