@surea11y/core 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +81 -7
  2. package/LICENSE +373 -21
  3. package/README.md +175 -35
  4. package/bin/surea11y-core.js +20 -0
  5. package/docs/API_STABILITY.md +27 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/ENGINE_OPTIONS.md +2 -0
  9. package/docs/I18N.md +12 -9
  10. package/docs/INTEGRATION.md +19 -1
  11. package/docs/LIMITATIONS.md +1 -1
  12. package/docs/OUTPUT_SCHEMA.md +1 -1
  13. package/docs/REPORT.md +1 -1
  14. package/docs/RULE_CATALOG.md +1 -1
  15. package/docs/SARIF.md +59 -0
  16. package/package.json +63 -18
  17. package/src/baseline.js +0 -0
  18. package/src/checks/automatic/area-alt-present.js +63 -31
  19. package/src/checks/automatic/aria-allowed-attr.js +204 -80
  20. package/src/checks/automatic/aria-allowed-role.js +23 -7
  21. package/src/checks/automatic/aria-braille-equivalent.js +34 -10
  22. package/src/checks/automatic/aria-conditional-attr.js +32 -14
  23. package/src/checks/automatic/aria-deprecated-role.js +26 -11
  24. package/src/checks/automatic/aria-hidden-body.js +48 -23
  25. package/src/checks/automatic/aria-hidden-focus.js +420 -66
  26. package/src/checks/automatic/aria-prohibited-attr.js +327 -60
  27. package/src/checks/automatic/aria-prohibited-children.js +111 -103
  28. package/src/checks/automatic/aria-required-attr.js +29 -15
  29. package/src/checks/automatic/aria-required-children.js +44 -24
  30. package/src/checks/automatic/aria-required-parent.js +64 -35
  31. package/src/checks/automatic/aria-role-name-present.js +49 -21
  32. package/src/checks/automatic/aria-roles-valid.js +24 -12
  33. package/src/checks/automatic/aria-valid-attr-value.js +46 -22
  34. package/src/checks/automatic/aria-valid-attr.js +19 -5
  35. package/src/checks/automatic/autocomplete-valid.js +76 -16
  36. package/src/checks/automatic/avoid-inline-spacing.js +23 -8
  37. package/src/checks/automatic/binary-control-name-present.js +62 -50
  38. package/src/checks/automatic/button-name-present.js +54 -24
  39. package/src/checks/automatic/bypass-blocks-present.js +51 -32
  40. package/src/checks/automatic/canvas-text-alternative-present.js +59 -26
  41. package/src/checks/automatic/combobox-name-present.js +40 -45
  42. package/src/checks/automatic/contrast-computable.js +363 -341
  43. package/src/checks/automatic/contrast-enhanced.js +489 -466
  44. package/src/checks/automatic/contrast-minimum.js +488 -465
  45. package/src/checks/automatic/css-orientation-lock.js +51 -35
  46. package/src/checks/automatic/definition-list-children-valid.js +46 -25
  47. package/src/checks/automatic/deprecated-elements-not-used.js +25 -9
  48. package/src/checks/automatic/dialog-name-present.js +47 -85
  49. package/src/checks/automatic/dlitem-parent-valid.js +25 -8
  50. package/src/checks/automatic/duplicate-id-aria.js +28 -9
  51. package/src/checks/automatic/embed-text-alternative-present.js +88 -35
  52. package/src/checks/automatic/form-control-programmatic-label-present.js +81 -196
  53. package/src/checks/automatic/form-control-single-label.js +50 -14
  54. package/src/checks/automatic/html-xml-lang-mismatch.js +36 -18
  55. package/src/checks/automatic/iframe-focusable-content.js +265 -22
  56. package/src/checks/automatic/iframe-name-present.js +33 -9
  57. package/src/checks/automatic/iframe-title-unique.js +32 -9
  58. package/src/checks/automatic/img-alt-present.js +54 -52
  59. package/src/checks/automatic/input-image-alt-present.js +141 -112
  60. package/src/checks/automatic/label-in-name.js +65 -41
  61. package/src/checks/automatic/language-page-present.js +111 -109
  62. package/src/checks/automatic/link-in-text-block.js +61 -19
  63. package/src/checks/automatic/link-name-present.js +47 -14
  64. package/src/checks/automatic/list-children-valid.js +40 -33
  65. package/src/checks/automatic/listbox-name-present.js +41 -19
  66. package/src/checks/automatic/listitem-parent-valid.js +48 -13
  67. package/src/checks/automatic/menuitem-name-present.js +41 -61
  68. package/src/checks/automatic/meta-refresh-no-exceptions.js +32 -11
  69. package/src/checks/automatic/meta-refresh-timing-absent.js +22 -6
  70. package/src/checks/automatic/meta-viewport-zoom-enabled.js +26 -7
  71. package/src/checks/automatic/meter-name-present.js +40 -36
  72. package/src/checks/automatic/nested-interactive-controls-absent.js +58 -15
  73. package/src/checks/automatic/object-text-alternative-present.js +93 -39
  74. package/src/checks/automatic/option-name-present.js +40 -21
  75. package/src/checks/automatic/page-title-present.js +19 -6
  76. package/src/checks/automatic/progressbar-name-present.js +49 -44
  77. package/src/checks/automatic/role-img-alt-present.js +211 -159
  78. package/src/checks/automatic/searchbox-name-present.js +41 -19
  79. package/src/checks/automatic/server-side-image-map-absent.js +27 -11
  80. package/src/checks/automatic/slider-name-present.js +42 -47
  81. package/src/checks/automatic/spinbutton-name-present.js +41 -19
  82. package/src/checks/automatic/summary-name-present.js +39 -17
  83. package/src/checks/automatic/svg-image-text-alternative-present.js +116 -47
  84. package/src/checks/automatic/svg-text-alternative-present.js +262 -230
  85. package/src/checks/automatic/tab-name-present.js +39 -60
  86. package/src/checks/automatic/table-headers-attr-valid.js +27 -10
  87. package/src/checks/automatic/table-th-has-data-cells.js +24 -8
  88. package/src/checks/automatic/target-size-minimum.js +123 -48
  89. package/src/checks/automatic/td-has-header.js +53 -12
  90. package/src/checks/automatic/textbox-name-present.js +41 -19
  91. package/src/checks/automatic/tooltip-name-present.js +39 -18
  92. package/src/checks/automatic/treeitem-name-present.js +40 -21
  93. package/src/checks/automatic/valid-lang.js +22 -6
  94. package/src/checks/automatic/video-poster-text-alternative-present.js +81 -36
  95. package/src/checks/manual/accesskeys-manual.js +17 -6
  96. package/src/checks/manual/area-alt-decorative-manual.js +194 -193
  97. package/src/checks/manual/area-alt-quality-manual.js +184 -141
  98. package/src/checks/manual/aria-checked-state-mismatch-manual.js +48 -34
  99. package/src/checks/manual/aria-text-manual.js +20 -11
  100. package/src/checks/manual/canvas-text-alternative-quality-manual.js +151 -114
  101. package/src/checks/manual/css-hidden-focus.js +375 -169
  102. package/src/checks/manual/embed-text-alternative-quality-manual.js +178 -162
  103. package/src/checks/manual/empty-heading-manual.js +41 -24
  104. package/src/checks/manual/empty-table-header-manual.js +69 -31
  105. package/src/checks/manual/focus-order-semantics-manual.js +60 -13
  106. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +209 -246
  107. package/src/checks/manual/heading-order-manual.js +50 -8
  108. package/src/checks/manual/identical-links-same-purpose-manual.js +36 -12
  109. package/src/checks/manual/image-redundant-alt-manual.js +38 -8
  110. package/src/checks/manual/img-alt-decorative-manual.js +133 -96
  111. package/src/checks/manual/img-alt-quality-manual.js +178 -127
  112. package/src/checks/manual/input-image-alt-decorative-manual.js +127 -92
  113. package/src/checks/manual/input-image-alt-quality-manual.js +127 -92
  114. package/src/checks/manual/label-title-only-manual.js +44 -28
  115. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -38
  116. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +85 -32
  117. package/src/checks/manual/landmark-main-is-top-level-manual.js +69 -27
  118. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +45 -33
  119. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +43 -31
  120. package/src/checks/manual/landmark-no-duplicate-main-manual.js +27 -21
  121. package/src/checks/manual/landmark-one-main-manual.js +38 -43
  122. package/src/checks/manual/landmark-unique-manual.js +78 -67
  123. package/src/checks/manual/link-name-quality-manual.js +45 -12
  124. package/src/checks/manual/media-transcript-present-manual.js +37 -22
  125. package/src/checks/manual/meta-viewport-large-manual.js +19 -6
  126. package/src/checks/manual/mouse-only-event-handlers-manual.js +40 -11
  127. package/src/checks/manual/no-autoplay-audio-manual.js +22 -6
  128. package/src/checks/manual/object-text-alternative-quality-manual.js +177 -154
  129. package/src/checks/manual/p-as-heading-manual.js +24 -7
  130. package/src/checks/manual/page-has-heading-one-manual.js +42 -32
  131. package/src/checks/manual/page-title-patterns-manual.js +80 -50
  132. package/src/checks/manual/presentation-role-conflict-manual.js +101 -47
  133. package/src/checks/manual/region-manual.js +244 -60
  134. package/src/checks/manual/scope-attr-valid-manual.js +13 -4
  135. package/src/checks/manual/scrollable-region-focusable-manual.js +39 -11
  136. package/src/checks/manual/skip-link-manual.js +42 -18
  137. package/src/checks/manual/svg-text-alternative-quality-manual.js +208 -165
  138. package/src/checks/manual/tabindex-manual.js +13 -4
  139. package/src/checks/manual/table-duplicate-name-manual.js +22 -11
  140. package/src/checks/manual/table-fake-caption-manual.js +48 -10
  141. package/src/checks/manual/video-caption-manual.js +17 -4
  142. package/src/checks/manual-review.js +58 -12
  143. package/src/core.js +41705 -29650
  144. package/src/index.js +2 -0
  145. package/src/report.js +109 -47
  146. package/src/sarif.js +190 -0
  147. package/surea11y.browser.js +37774 -0
  148. package/bin/core.js +0 -348
  149. package/docs/CLI.md +0 -75
  150. package/src/catalogs/composites.wcag.js +0 -490
  151. package/src/checks/rules-and-tags.full.csv +0 -19
  152. package/src/checks/rules-and-tags.full.json +0 -259
  153. package/src/core/aria-helpers.js +0 -970
  154. package/src/core/contrast-helpers.js +0 -1147
  155. package/src/core/dom-helpers.js +0 -4235
  156. package/src/core/dom-runner.js +0 -671
  157. package/src/core/frame-messaging.js +0 -210
  158. package/src/core/frame-scan.js +0 -178
  159. package/src/core/rollup-composites.js +0 -135
  160. package/src/core/rule-meta.js +0 -159
  161. package/src/coverage/wcag-facets.js +0 -1079
  162. package/src/coverage/wcag-version-map.js +0 -84
  163. package/src/i18n/en.js +0 -923
  164. package/src/i18n/fr.js +0 -844
  165. package/src/policy/contracts.js +0 -18
  166. package/src/policy/resolvePolicy.js +0 -55
  167. package/src/policy/schemas/engine-options.schema.json +0 -103
  168. package/src/policy/schemas/policy-contract.schema.json +0 -40
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -20,10 +22,9 @@
20
22
  * technology — activating the outer control and the inner one become
21
23
  * ambiguous, and some AT only exposes one of the two.
22
24
  * @implementation-notes
23
- * - Matches a widely-used reference engine's nested-interactive. Reports on the outer
24
- * (containing) control, not the nested descendant — this engine
25
- * considers the container the fixable unit ("move the nested control
26
- * outside this element").
25
+ * - Reports on the outer (containing) control, not the nested descendant —
26
+ * this engine considers the container the fixable unit ("move the nested
27
+ * control outside this element").
27
28
  * - A container can be reported once even with multiple nested
28
29
  * descendants (listed together); a deeply nested chain (A > B > C, all
29
30
  * interactive) reports both A and B as separate occurrences, since each
@@ -34,7 +35,8 @@ const id = 'nested-interactive-controls-absent';
34
35
 
35
36
  const meta = {
36
37
  title: 'Interactive controls must not be nested',
37
- description: 'Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.',
38
+ description:
39
+ 'Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.',
38
40
  i18n: {
39
41
  titleKey: 'nestedInteractiveControlsAbsent_title',
40
42
  descriptionKey: 'nestedInteractiveControlsAbsent_description'
@@ -43,7 +45,13 @@ const meta = {
43
45
  tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic'],
44
46
  wcagSc: ['4.1.2'],
45
47
  normativeMappings: [
46
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
48
+ {
49
+ standard: 'WCAG',
50
+ version: '2.2',
51
+ requirement: '4.1.2',
52
+ title: 'Name, Role, Value',
53
+ conformanceLevel: 'A'
54
+ }
47
55
  ],
48
56
  defaultSeverity: 'serious',
49
57
  category: 'robust',
@@ -53,7 +61,7 @@ const meta = {
53
61
  };
54
62
 
55
63
  function runInPage(ctx) {
56
- const { document, helpers, rule } = ctx;
64
+ const { helpers, rule } = ctx;
57
65
 
58
66
  // Declared inside runInPage — see scripts/build-core.js header
59
67
  // ("runInPage MUST be self-contained").
@@ -82,29 +90,55 @@ function runInPage(ctx) {
82
90
  '[role="treeitem"]'
83
91
  ].join(', ');
84
92
 
85
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(INTERACTIVE_SELECTOR) : helpers.queryAll(INTERACTIVE_SELECTOR);
93
+ const isAccTreeEligible =
94
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
95
+
96
+ function isEligible(node) {
97
+ if (!isAccTreeEligible) return true;
98
+ try {
99
+ const r = isAccTreeEligible(node, ctx);
100
+ return !!(r && r.eligible);
101
+ } catch {
102
+ return true;
103
+ }
104
+ }
105
+
106
+ const nodes = helpers.queryAllSmart
107
+ ? helpers.queryAllSmart(INTERACTIVE_SELECTOR)
108
+ : helpers.queryAll(INTERACTIVE_SELECTOR);
86
109
 
87
110
  const occurrences = [];
88
111
  let applicableCount = 0;
89
112
 
90
113
  for (const el of nodes) {
91
114
  if (!el || !el.querySelectorAll) continue;
115
+ if (!isEligible(el)) continue;
92
116
 
93
117
  applicableCount += 1;
94
118
 
95
- let nested = [];
119
+ // The nested search uses the raw native querySelectorAll (not
120
+ // helpers.queryAllSmart), so it isn't subject to that helper's
121
+ // default hidden-content policy at all -- not even hard CSS-based
122
+ // hiding, let alone aria-hidden. A nested descendant that is never
123
+ // actually rendered or exposed to AT (display:none, aria-hidden,
124
+ // etc.) creates no real ambiguity for ANY user, since it isn't there
125
+ // to be confused with the outer control.
126
+ let nested;
96
127
  try {
97
- nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
128
+ nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR)).filter(isEligible);
98
129
  } catch {
99
130
  nested = [];
100
131
  }
101
132
  if (!nested.length) continue;
102
133
 
103
134
  const nestedTags = nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown'));
135
+ const dedupedNestedTags = [
136
+ ...new Set(nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown')))
137
+ ];
104
138
 
105
139
  const tag = el.tagName.toLowerCase();
106
140
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
107
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
141
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
108
142
 
109
143
  occurrences.push({
110
144
  selector: stableSelector,
@@ -114,10 +148,14 @@ function runInPage(ctx) {
114
148
  i18n: {
115
149
  summaryKey: 'nestedInteractiveControlsAbsent_summary_fail',
116
150
  hintKey: 'nestedInteractiveControlsAbsent_hint_fail',
117
- params: { element: tag, nestedElements: nestedTags.join(', ') }
151
+ params: { element: tag, nestedElements: dedupedNestedTags.join(', ') }
118
152
  },
119
153
  data: {
120
- details: { reasonCode: 'NESTED_INTERACTIVE_CONTROL', element: tag, nestedElements: nestedTags }
154
+ details: {
155
+ reasonCode: 'NESTED_INTERACTIVE_CONTROL',
156
+ element: tag,
157
+ nestedElements: nestedTags
158
+ }
121
159
  }
122
160
  });
123
161
  }
@@ -126,9 +164,14 @@ function runInPage(ctx) {
126
164
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
127
165
  }
128
166
  if (occurrences.length) {
129
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
167
+ return {
168
+ ruleId: rule.ruleId,
169
+ outcome: 'fail',
170
+ severity: rule.defaultSeverity || 'serious',
171
+ occurrences
172
+ };
130
173
  }
131
174
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
132
175
  }
133
176
 
134
- module.exports = { id, meta, runInPage };
177
+ module.exports = { id, meta, runInPage };
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  /**
@@ -22,7 +24,8 @@ const id = 'object-text-alternative-present';
22
24
 
23
25
  const meta = {
24
26
  title: '<object> must provide a text alternative',
25
- description: 'Checks that <object> elements provide a text alternative via fallback content or an accessible name.',
27
+ description:
28
+ 'Checks that <object> elements provide a text alternative via fallback content or an accessible name.',
26
29
  i18n: {
27
30
  titleKey: 'object_textAltPresent_title',
28
31
  descriptionKey: 'object_textAltPresent_description'
@@ -31,7 +34,13 @@ const meta = {
31
34
  tags: ['wcag2a', 'wcag111', 'nontext', 'object', 'atomic', 'automatic'],
32
35
  wcagSc: ['1.1.1'],
33
36
  normativeMappings: [
34
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
37
+ {
38
+ standard: 'WCAG',
39
+ version: '2.2',
40
+ requirement: '1.1.1',
41
+ title: 'Non-text Content',
42
+ conformanceLevel: 'A'
43
+ }
35
44
  ],
36
45
  defaultSeverity: 'serious',
37
46
  category: 'perceivable',
@@ -44,33 +53,39 @@ function runInPage(ctx) {
44
53
  const { document, root, helpers, rule } = ctx;
45
54
  const safeRoot = root || document;
46
55
 
47
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
48
- const queryAll = helpers && typeof helpers.queryAll === 'function'
49
- ? helpers.queryAll
50
- : (sel) => {
51
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
52
- catch { return []; }
53
- };
54
-
55
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
56
- ? helpers.getEligibilityInfo
57
- : null;
58
-
59
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
60
- ? helpers.isAccTreeEligible
61
- : null;
62
-
63
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
64
- ? helpers.getFocusableInfo
65
- : null;
66
-
67
- const getAriaNameInfo = helpers && typeof helpers.getAriaNameInfo === 'function'
68
- ? helpers.getAriaNameInfo
69
- : null;
56
+ const queryAllSmart =
57
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
58
+ const queryAll =
59
+ helpers && typeof helpers.queryAll === 'function'
60
+ ? helpers.queryAll
61
+ : (sel) => {
62
+ try {
63
+ return safeRoot && safeRoot.querySelectorAll
64
+ ? Array.from(safeRoot.querySelectorAll(sel))
65
+ : [];
66
+ } catch {
67
+ return [];
68
+ }
69
+ };
70
+
71
+ const getEligibilityInfo =
72
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
73
+
74
+ const isAccTreeEligible =
75
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
76
+
77
+ const getFocusableInfo =
78
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
79
+
80
+ const getAriaNameInfo =
81
+ helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
70
82
 
71
83
  const objects = (() => {
72
- try { return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []); }
73
- catch { return queryAll('object'); }
84
+ try {
85
+ return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []);
86
+ } catch {
87
+ return queryAll('object');
88
+ }
74
89
  })();
75
90
 
76
91
  if (!objects.length) {
@@ -88,11 +103,22 @@ function runInPage(ctx) {
88
103
  let aria = null;
89
104
 
90
105
  if (getAriaNameInfo) {
91
- aria = (() => { try { return getAriaNameInfo(el, ctx); } catch { return null; } })();
106
+ aria = (() => {
107
+ try {
108
+ return getAriaNameInfo(el, ctx);
109
+ } catch {
110
+ return null;
111
+ }
112
+ })();
92
113
  }
93
114
 
94
115
  if (aria && aria.present && trim(aria.value)) {
95
- return { present: true, value: trim(aria.value), mechanism: aria.mechanism || 'aria', flags: (aria.flags || []).slice(0) };
116
+ return {
117
+ present: true,
118
+ value: trim(aria.value),
119
+ mechanism: aria.mechanism || 'aria',
120
+ flags: (aria.flags || []).slice(0)
121
+ };
96
122
  }
97
123
 
98
124
  const title = trim(el.getAttribute && el.getAttribute('title'));
@@ -123,20 +149,43 @@ function runInPage(ctx) {
123
149
  if (!el || !el.getAttribute) continue;
124
150
 
125
151
  if (isAccTreeEligible) {
126
- const elig = (() => { try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; } })();
152
+ const elig = (() => {
153
+ try {
154
+ return isAccTreeEligible(el, ctx);
155
+ } catch {
156
+ return { eligible: true, reasons: [] };
157
+ }
158
+ })();
127
159
  if (elig && elig.eligible === false) continue;
128
160
  }
129
161
 
130
162
  // role presentation/none exclusion only when not focusable
131
- const role = (() => { try { return String(el.getAttribute('role') || '').trim().toLowerCase(); } catch { return ''; } })();
163
+ const role = (() => {
164
+ try {
165
+ return String(el.getAttribute('role') || '')
166
+ .trim()
167
+ .toLowerCase();
168
+ } catch {
169
+ return '';
170
+ }
171
+ })();
132
172
  if (role === 'presentation' || role === 'none') {
133
- let focusable = false;
173
+ let focusable;
134
174
  if (getFocusableInfo) {
135
- const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
175
+ const fi = (() => {
176
+ try {
177
+ return getFocusableInfo(el, ctx);
178
+ } catch {
179
+ return null;
180
+ }
181
+ })();
136
182
  focusable = !!(fi && fi.focusable);
137
183
  } else {
138
184
  const tabindex = el.getAttribute('tabindex');
139
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
185
+ focusable =
186
+ tabindex != null &&
187
+ String(tabindex).trim() !== '' &&
188
+ !Number.isNaN(Number(String(tabindex).trim()));
140
189
  }
141
190
  if (!focusable) continue;
142
191
  }
@@ -147,18 +196,18 @@ function runInPage(ctx) {
147
196
 
148
197
  // Only compute fallback text if there is no name.
149
198
  // (textContent can be expensive; avoid when not needed)
150
- const fb = name.present ? { present: false, value: '', mechanism: 'fallback', flags: ['skipped-name-present'] }
151
- : computeFallbackText(el);
199
+ const fb = name.present
200
+ ? { present: false, value: '', mechanism: 'fallback', flags: ['skipped-name-present'] }
201
+ : computeFallbackText(el);
152
202
 
153
203
  const hasTextAlt = !!(name.present || fb.present);
154
204
  if (hasTextAlt) continue;
155
205
 
156
-
157
206
  const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
158
207
 
159
208
  const baseOccurrence = {
160
209
  summary: 'Missing text alternative for <object>.',
161
- hint: 'Provide meaningful fallback content inside <object>, or add an accessible name (aria-label/aria-labelledby).',
210
+ hint: 'Provide meaningful fallback content inside <object>, add an accessible name (aria-label/aria-labelledby), or use a title attribute as a best-effort fallback.',
162
211
  i18n: {
163
212
  summaryKey: 'object_textAltPresent_summary_fail',
164
213
  hintKey: 'object_textAltPresent_hint_fail',
@@ -187,7 +236,12 @@ function runInPage(ctx) {
187
236
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
188
237
  }
189
238
 
190
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
239
+ return {
240
+ ruleId: rule.ruleId,
241
+ outcome: 'fail',
242
+ severity: rule.defaultSeverity || 'minor',
243
+ occurrences
244
+ };
191
245
  }
192
246
 
193
247
  module.exports = { id, meta, runInPage };
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'option-name-present';
@@ -10,10 +12,16 @@ const meta = {
10
12
  descriptionKey: 'optionNamePresent_description'
11
13
  },
12
14
  helpUrl: null,
13
- tags: ["wcag2a", "wcag412", "forms", "atomic", "automatic", "name", "option"],
15
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'option'],
14
16
  wcagSc: ['4.1.2'],
15
17
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
18
+ {
19
+ standard: 'WCAG',
20
+ version: '2.2',
21
+ requirement: '4.1.2',
22
+ title: 'Name, Role, Value',
23
+ conformanceLevel: 'A'
24
+ }
17
25
  ],
18
26
  defaultSeverity: 'serious',
19
27
  category: 'robust',
@@ -24,20 +32,22 @@ const meta = {
24
32
 
25
33
  function runInPage(ctx) {
26
34
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
35
+ const getEligibilityInfo =
36
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
37
 
32
38
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
39
+ return String(s || '')
40
+ .replace(/\s+/g, ' ')
41
+ .trim();
34
42
  }
35
43
 
36
44
  function getAttr(el, name) {
37
45
  try {
38
46
  if (!el || !el.getAttribute) return '';
39
47
  return normalizeWs(el.getAttribute(name));
40
- } catch { return ''; }
48
+ } catch {
49
+ return '';
50
+ }
41
51
  }
42
52
 
43
53
  function getConservativeSubtreeText(document, container) {
@@ -52,7 +62,7 @@ function runInPage(ctx) {
52
62
  const info = helpers.getContentNameInfo(container, ctx);
53
63
  return info && info.present ? info.value : '';
54
64
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
65
+ const t = container && container.textContent ? String(container.textContent) : '';
56
66
  return t.replace(/\s+/g, ' ').trim();
57
67
  }
58
68
 
@@ -74,7 +84,8 @@ function runInPage(ctx) {
74
84
  }
75
85
 
76
86
  function isEligibleAcc(helpers, el, ctx) {
77
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
87
+ const fn =
88
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
89
  if (!fn) return true;
79
90
  try {
80
91
  const r = fn(el, ctx);
@@ -85,13 +96,13 @@ function runInPage(ctx) {
85
96
  }
86
97
  }
87
98
 
88
-
89
99
  const occurrences = [];
90
100
  let applicableCount = 0;
91
101
 
92
- const selector = "[role=\"option\"]";
93
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
94
-
102
+ const selector = '[role="option"]';
103
+ const nodes = helpers.queryAllSmart
104
+ ? helpers.queryAllSmart(selector)
105
+ : helpers.queryAll(selector);
95
106
 
96
107
  function hasName(el) {
97
108
  const ariaLabel = getAttr(el, 'aria-label');
@@ -103,11 +114,9 @@ function runInPage(ctx) {
103
114
  const title = getAttr(el, 'title');
104
115
  if (title) return { ok: true, method: 'title' };
105
116
 
106
-
107
117
  const t = getConservativeSubtreeText(document, el);
108
118
  if (t) return { ok: true, method: 'content' };
109
119
 
110
-
111
120
  return { ok: false, method: 'none' };
112
121
  }
113
122
 
@@ -121,10 +130,16 @@ function runInPage(ctx) {
121
130
  if (res.ok) continue;
122
131
 
123
132
  const eligInfo = getEligibilityInfo
124
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
125
- : null;
133
+ ? (() => {
134
+ try {
135
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
136
+ } catch {
137
+ return null;
138
+ }
139
+ })()
140
+ : null;
126
141
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
127
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
142
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
128
143
 
129
144
  occurrences.push({
130
145
  selector: stableSelector,
@@ -143,12 +158,16 @@ function runInPage(ctx) {
143
158
  });
144
159
  }
145
160
 
146
-
147
161
  if (applicableCount === 0) {
148
162
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
149
163
  }
150
164
  if (occurrences.length) {
151
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
165
+ return {
166
+ ruleId: rule.ruleId,
167
+ outcome: 'fail',
168
+ severity: rule.defaultSeverity || 'minor',
169
+ occurrences
170
+ };
152
171
  }
153
172
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
154
173
  }
@@ -1,3 +1,5 @@
1
+ /* SPDX-License-Identifier: MPL-2.0 */
2
+
1
3
  'use strict';
2
4
 
3
5
  const id = 'page-title-present';
@@ -13,7 +15,13 @@ const meta = {
13
15
  tags: ['wcag2a', 'wcag242', 'titles', 'atomic', 'navigation', 'automatic'],
14
16
  wcagSc: ['2.4.2'],
15
17
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '2.4.2', title: 'Page Titled', conformanceLevel: 'A' }
18
+ {
19
+ standard: 'WCAG',
20
+ version: '2.2',
21
+ requirement: '2.4.2',
22
+ title: 'Page Titled',
23
+ conformanceLevel: 'A'
24
+ }
17
25
  ],
18
26
  defaultSeverity: 'serious',
19
27
  category: 'operable',
@@ -54,13 +62,13 @@ function runInPage(ctx) {
54
62
  selector: 'head > title',
55
63
  html: '',
56
64
  summary: missingTitleEl
57
- ? 'The page is missing a <title> element.'
58
- : 'The page has an empty <title>.',
65
+ ? 'The page is missing a <title> element.'
66
+ : 'The page has an empty <title>.',
59
67
  hint: 'Provide a descriptive, non-empty <title> that identifies the page topic or purpose.',
60
68
  i18n: {
61
69
  summaryKey: missingTitleEl
62
- ? 'pageTitlePresent_summary_fail_missing'
63
- : 'pageTitlePresent_summary_fail_empty',
70
+ ? 'pageTitlePresent_summary_fail_missing'
71
+ : 'pageTitlePresent_summary_fail_empty',
64
72
  hintKey: 'pageTitlePresent_hint_fail',
65
73
  params: {}
66
74
  },
@@ -86,7 +94,12 @@ function runInPage(ctx) {
86
94
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
87
95
  }
88
96
  if (occurrences.length) {
89
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
97
+ return {
98
+ ruleId: rule.ruleId,
99
+ outcome: 'fail',
100
+ severity: rule.defaultSeverity || 'minor',
101
+ occurrences
102
+ };
90
103
  }
91
104
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
92
105
  }