pour-engine 1.28.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 +18 -17
  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 +117 -37
  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.28.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
 
@@ -185,7 +177,9 @@ export async function run(context = document, options = {}, onProgress) {
185
177
 
186
178
  let outcomes;
187
179
  if (rule.evaluateAll) {
188
- outcomes = rule.evaluateAll(elements, ruleHelpers);
180
+ // Awaiting a plain array is a no-op, so sync evaluateAll rules are
181
+ // untouched — async ones (the deferred-reference probe) resolve here.
182
+ outcomes = await rule.evaluateAll(elements, ruleHelpers);
189
183
  } else {
190
184
  // Evaluate sequentially with periodic yields so ONE heavy rule on a
191
185
  // huge element set can't freeze the page for its whole duration —
@@ -221,17 +215,15 @@ export async function run(context = document, options = {}, onProgress) {
221
215
 
222
216
  const ruleResult = {
223
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,
224
221
  impact: rule.impact,
225
222
  tags: rule.tags,
226
223
  help: rule.help,
227
224
  description: rule.help,
228
225
  helpUrl: rule.helpUrl,
229
226
  };
230
- if (draft30) {
231
- // Which draft guideline this 2.x rule speaks to (null for rules with
232
- // no draft counterpart, e.g. best-practice extras).
233
- ruleResult.wcag30 = wcag30Draft.guidelineForTags(rule.tags);
234
- }
235
227
  if (!elements.length) results.inapplicable.push({ ...ruleResult, nodes: [] });
236
228
  if (buckets.fail.length) results.violations.push({ ...ruleResult, nodes: buckets.fail });
237
229
  if (buckets.incomplete.length) results.incomplete.push({ ...ruleResult, nodes: buckets.incomplete });
@@ -250,6 +242,15 @@ export async function run(context = document, options = {}, onProgress) {
250
242
  ms,
251
243
  counts: { ...liveCounts },
252
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
+ } : {}),
253
254
  });
254
255
  }
255
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',