@surea11y/core 1.2.0 → 1.3.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 (157) hide show
  1. package/CHANGELOG.md +37 -7
  2. package/LICENSE +373 -21
  3. package/README.md +67 -1
  4. package/bin/core.js +144 -19
  5. package/docs/API_STABILITY.md +1 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/CLI.md +54 -1
  9. package/docs/ENGINE_OPTIONS.md +2 -0
  10. package/docs/INTEGRATION.md +18 -0
  11. package/docs/OUTPUT_SCHEMA.md +1 -1
  12. package/docs/RULE_CATALOG.md +1 -1
  13. package/docs/SARIF.md +59 -0
  14. package/package.json +15 -4
  15. package/src/baseline.js +0 -0
  16. package/src/catalogs/composites.wcag.js +414 -450
  17. package/src/checks/automatic/area-alt-present.js +59 -25
  18. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  19. package/src/checks/automatic/aria-allowed-role.js +21 -7
  20. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  21. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  22. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  23. package/src/checks/automatic/aria-hidden-body.js +42 -19
  24. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  25. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  26. package/src/checks/automatic/aria-prohibited-children.js +55 -16
  27. package/src/checks/automatic/aria-required-attr.js +23 -8
  28. package/src/checks/automatic/aria-required-children.js +37 -14
  29. package/src/checks/automatic/aria-required-parent.js +48 -14
  30. package/src/checks/automatic/aria-role-name-present.js +47 -21
  31. package/src/checks/automatic/aria-roles-valid.js +22 -12
  32. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  33. package/src/checks/automatic/aria-valid-attr.js +17 -5
  34. package/src/checks/automatic/autocomplete-valid.js +74 -16
  35. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  36. package/src/checks/automatic/binary-control-name-present.js +60 -50
  37. package/src/checks/automatic/button-name-present.js +48 -18
  38. package/src/checks/automatic/bypass-blocks-present.js +42 -25
  39. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  40. package/src/checks/automatic/combobox-name-present.js +38 -45
  41. package/src/checks/automatic/contrast-computable.js +361 -341
  42. package/src/checks/automatic/contrast-enhanced.js +487 -466
  43. package/src/checks/automatic/contrast-minimum.js +486 -465
  44. package/src/checks/automatic/css-orientation-lock.js +30 -8
  45. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  46. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  47. package/src/checks/automatic/dialog-name-present.js +37 -75
  48. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  49. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  50. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  51. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  52. package/src/checks/automatic/form-control-single-label.js +47 -10
  53. package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
  54. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  55. package/src/checks/automatic/iframe-name-present.js +31 -9
  56. package/src/checks/automatic/iframe-title-unique.js +29 -8
  57. package/src/checks/automatic/img-alt-present.js +47 -43
  58. package/src/checks/automatic/input-image-alt-present.js +143 -112
  59. package/src/checks/automatic/label-in-name.js +50 -22
  60. package/src/checks/automatic/language-page-present.js +109 -109
  61. package/src/checks/automatic/link-in-text-block.js +59 -19
  62. package/src/checks/automatic/link-name-present.js +45 -14
  63. package/src/checks/automatic/list-children-valid.js +26 -9
  64. package/src/checks/automatic/listbox-name-present.js +39 -19
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  66. package/src/checks/automatic/menuitem-name-present.js +39 -61
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
  70. package/src/checks/automatic/meter-name-present.js +36 -33
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  72. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  73. package/src/checks/automatic/option-name-present.js +38 -21
  74. package/src/checks/automatic/page-title-present.js +17 -6
  75. package/src/checks/automatic/progressbar-name-present.js +41 -34
  76. package/src/checks/automatic/role-img-alt-present.js +209 -157
  77. package/src/checks/automatic/searchbox-name-present.js +39 -19
  78. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  79. package/src/checks/automatic/slider-name-present.js +40 -47
  80. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  81. package/src/checks/automatic/summary-name-present.js +37 -17
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  83. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  84. package/src/checks/automatic/tab-name-present.js +37 -60
  85. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  86. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  87. package/src/checks/automatic/target-size-minimum.js +118 -48
  88. package/src/checks/automatic/td-has-header.js +29 -11
  89. package/src/checks/automatic/textbox-name-present.js +39 -19
  90. package/src/checks/automatic/tooltip-name-present.js +37 -18
  91. package/src/checks/automatic/treeitem-name-present.js +38 -21
  92. package/src/checks/automatic/valid-lang.js +20 -6
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  94. package/src/checks/manual/accesskeys-manual.js +14 -5
  95. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  96. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  98. package/src/checks/manual/aria-text-manual.js +14 -6
  99. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  100. package/src/checks/manual/css-hidden-focus.js +196 -165
  101. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  102. package/src/checks/manual/empty-heading-manual.js +24 -7
  103. package/src/checks/manual/empty-table-header-manual.js +17 -6
  104. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  105. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  106. package/src/checks/manual/heading-order-manual.js +22 -7
  107. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  108. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  109. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  110. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  111. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  112. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  113. package/src/checks/manual/label-title-only-manual.js +14 -5
  114. package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
  115. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
  116. package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
  117. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
  118. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
  119. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  120. package/src/checks/manual/landmark-one-main-manual.js +26 -20
  121. package/src/checks/manual/landmark-unique-manual.js +41 -15
  122. package/src/checks/manual/link-name-quality-manual.js +43 -12
  123. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  124. package/src/checks/manual/meta-viewport-large-manual.js +16 -5
  125. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  126. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  127. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  128. package/src/checks/manual/p-as-heading-manual.js +22 -7
  129. package/src/checks/manual/page-has-heading-one-manual.js +31 -22
  130. package/src/checks/manual/page-title-patterns-manual.js +78 -50
  131. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  132. package/src/checks/manual/region-manual.js +241 -48
  133. package/src/checks/manual/scope-attr-valid-manual.js +10 -3
  134. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  135. package/src/checks/manual/skip-link-manual.js +35 -12
  136. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  137. package/src/checks/manual/tabindex-manual.js +10 -3
  138. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  139. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  140. package/src/checks/manual/video-caption-manual.js +15 -4
  141. package/src/checks/manual-review.js +56 -12
  142. package/src/core/aria-helpers.js +1127 -886
  143. package/src/core/contrast-helpers.js +1217 -1062
  144. package/src/core/dom-helpers.js +4175 -3917
  145. package/src/core/dom-runner.js +720 -604
  146. package/src/core/frame-messaging.js +189 -138
  147. package/src/core/frame-scan.js +94 -82
  148. package/src/core/rollup-composites.js +94 -102
  149. package/src/core/rule-meta.js +58 -41
  150. package/src/core.js +36552 -29111
  151. package/src/i18n/en.js +1194 -889
  152. package/src/i18n/fr.js +1136 -795
  153. package/src/policy/contracts.js +13 -13
  154. package/src/policy/resolvePolicy.js +48 -44
  155. package/src/report.js +63 -43
  156. package/src/sarif.js +175 -0
  157. package/surea11y.browser.js +36042 -0
@@ -11,194 +11,205 @@
11
11
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
12
  */
13
13
 
14
- const id = "embed-text-alternative-quality";
14
+ const id = 'embed-text-alternative-quality';
15
15
 
16
16
  const meta = {
17
- title: "<embed> text alternative must be appropriate (manual review)",
18
- description: "Flags <embed> elements with a detected name for human review of appropriateness.",
19
- i18n: {
20
- titleKey: "embed_textAltQuality_title",
21
- descriptionKey: "embed_textAltQuality_description"
22
- },
23
- helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "nontext", "embed", "manual", "atomic"],
25
- wcagSc: ['1.1.1'],
26
- normativeMappings: [
27
- {standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A'}
28
- ],
29
- defaultSeverity: 'minor',
30
- category: 'perceivable',
31
- type: 'manual',
32
- defaultConfidence: 'medium',
33
- coverage: {
34
- facetsBySc: {
35
- '1.1.1': ['text-alternative-quality']
36
- }
17
+ title: '<embed> text alternative must be appropriate (manual review)',
18
+ description: 'Flags <embed> elements with a detected name for human review of appropriateness.',
19
+ i18n: {
20
+ titleKey: 'embed_textAltQuality_title',
21
+ descriptionKey: 'embed_textAltQuality_description'
22
+ },
23
+ helpUrl: null,
24
+ tags: ['wcag2a', 'wcag111', 'nontext', 'embed', 'manual', 'atomic'],
25
+ wcagSc: ['1.1.1'],
26
+ normativeMappings: [
27
+ {
28
+ standard: 'WCAG',
29
+ version: '2.2',
30
+ requirement: '1.1.1',
31
+ title: 'Non-text Content',
32
+ conformanceLevel: 'A'
33
+ }
34
+ ],
35
+ defaultSeverity: 'minor',
36
+ category: 'perceivable',
37
+ type: 'manual',
38
+ defaultConfidence: 'medium',
39
+ coverage: {
40
+ facetsBySc: {
41
+ '1.1.1': ['text-alternative-quality']
37
42
  }
43
+ }
38
44
  };
39
45
 
40
46
  function runInPage(ctx) {
41
- const {document, root, helpers, rule} = ctx;
42
- const safeRoot = root || document;
43
-
44
- const trim = (v) => (v == null ? '' : String(v)).trim();
45
- const getTextFromIdRefs =
46
- helpers && typeof helpers.getTextFromIdRefs === 'function' ? helpers.getTextFromIdRefs : null;
47
-
48
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
49
- const queryAll = helpers && typeof helpers.queryAll === 'function'
50
- ? helpers.queryAll
51
- : (sel) => {
52
- try {
53
- return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : [];
54
- } catch {
55
- return [];
56
- }
47
+ const { document, root, helpers, rule } = ctx;
48
+ const safeRoot = root || document;
49
+
50
+ const trim = (v) => (v == null ? '' : String(v)).trim();
51
+ const getTextFromIdRefs =
52
+ helpers && typeof helpers.getTextFromIdRefs === 'function' ? helpers.getTextFromIdRefs : null;
53
+
54
+ const queryAllSmart =
55
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
56
+ const queryAll =
57
+ helpers && typeof helpers.queryAll === 'function'
58
+ ? helpers.queryAll
59
+ : (sel) => {
60
+ try {
61
+ return safeRoot && safeRoot.querySelectorAll
62
+ ? Array.from(safeRoot.querySelectorAll(sel))
63
+ : [];
64
+ } catch {
65
+ return [];
66
+ }
57
67
  };
58
68
 
59
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
60
- ? helpers.getEligibilityInfo
61
- : null;
62
-
63
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
64
- ? helpers.isAccTreeEligible
65
- : null;
66
-
67
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
68
- ? helpers.getFocusableInfo
69
- : null;
70
-
71
- function isRolePresentationExcluded(el) {
72
- const role = (() => {
73
- try {
74
- return String(el.getAttribute('role') || '').trim().toLowerCase();
75
- } catch {
76
- return '';
77
- }
78
- })();
79
- if (role !== 'presentation' && role !== 'none') return false;
80
-
81
- // Exclude only when NOT focusable (mirrors img-alt-present policy)
82
- let focusable = false;
83
- if (getFocusableInfo) {
84
- const fi = (() => {
85
- try {
86
- return getFocusableInfo(el, ctx);
87
- } catch {
88
- return null;
89
- }
90
- })();
91
- focusable = !!(fi && fi.focusable);
92
- } else {
93
- const tabindex = el.getAttribute('tabindex');
94
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
95
- }
96
- return !focusable;
97
- }
69
+ const getEligibilityInfo =
70
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
71
+
72
+ const isAccTreeEligible =
73
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
98
74
 
75
+ const getFocusableInfo =
76
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
99
77
 
100
- const els = (() => {
78
+ function isRolePresentationExcluded(el) {
79
+ const role = (() => {
80
+ try {
81
+ return String(el.getAttribute('role') || '')
82
+ .trim()
83
+ .toLowerCase();
84
+ } catch {
85
+ return '';
86
+ }
87
+ })();
88
+ if (role !== 'presentation' && role !== 'none') return false;
89
+
90
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
91
+ let focusable;
92
+ if (getFocusableInfo) {
93
+ const fi = (() => {
101
94
  try {
102
- return Array.from((queryAllSmart ? queryAllSmart('embed') : queryAll('embed')) || []);
95
+ return getFocusableInfo(el, ctx);
103
96
  } catch {
104
- return queryAll('embed');
97
+ return null;
105
98
  }
106
- })();
107
-
108
- if (!els.length) {
109
- return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
99
+ })();
100
+ focusable = !!(fi && fi.focusable);
101
+ } else {
102
+ const tabindex = el.getAttribute('tabindex');
103
+ focusable =
104
+ tabindex != null &&
105
+ String(tabindex).trim() !== '' &&
106
+ !Number.isNaN(Number(String(tabindex).trim()));
110
107
  }
108
+ return !focusable;
109
+ }
110
+
111
+ const els = (() => {
112
+ try {
113
+ return Array.from((queryAllSmart ? queryAllSmart('embed') : queryAll('embed')) || []);
114
+ } catch {
115
+ return queryAll('embed');
116
+ }
117
+ })();
111
118
 
112
- const occurrences = [];
113
- let applicableCount = 0;
114
-
115
- for (const el of els) {
116
- if (!el || !el.getAttribute) continue;
117
-
118
- if (isAccTreeEligible) {
119
- const elig = (() => {
120
- try {
121
- return isAccTreeEligible(el, ctx);
122
- } catch {
123
- return {eligible: true, reasons: []};
124
- }
125
- })();
126
- if (elig && elig.eligible === false) continue;
127
- }
119
+ if (!els.length) {
120
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
121
+ }
128
122
 
129
- if (isRolePresentationExcluded(el)) continue;
123
+ const occurrences = [];
124
+ let applicableCount = 0;
130
125
 
131
- let ariaLabel = '';
132
- let ariaLabelledBy = '';
133
- let title = '';
126
+ for (const el of els) {
127
+ if (!el || !el.getAttribute) continue;
128
+
129
+ if (isAccTreeEligible) {
130
+ const elig = (() => {
134
131
  try {
135
- ariaLabel = trim(el.getAttribute('aria-label'));
136
- ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
137
- title = trim(el.getAttribute('title'));
132
+ return isAccTreeEligible(el, ctx);
138
133
  } catch {
139
- ariaLabel = '';
140
- ariaLabelledBy = '';
141
- title = '';
142
- }
143
-
144
- // Only resolve IDREF text if aria-labelledby is present and aria-label is not already sufficient
145
- let labelledByText = '';
146
- let hasLabelledByMechanism = false;
147
-
148
- if (!ariaLabel && ariaLabelledBy) {
149
- hasLabelledByMechanism = true;
150
-
151
- if (getTextFromIdRefs) {
152
- try {
153
- const t = getTextFromIdRefs(ariaLabelledBy, ctx);
154
- labelledByText = trim(t && t.text);
155
- // If it resolves to empty, still treat as a mechanism present for manual review.
156
- } catch {
157
- labelledByText = '';
158
- }
159
- }
134
+ return { eligible: true, reasons: [] };
160
135
  }
136
+ })();
137
+ if (elig && elig.eligible === false) continue;
138
+ }
161
139
 
162
- const hasNameMechanism = !!(ariaLabel || title || hasLabelledByMechanism);
163
- if (!hasNameMechanism) continue;
140
+ if (isRolePresentationExcluded(el)) continue;
141
+
142
+ let ariaLabel;
143
+ let ariaLabelledBy;
144
+ let title;
145
+ try {
146
+ ariaLabel = trim(el.getAttribute('aria-label'));
147
+ ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
148
+ title = trim(el.getAttribute('title'));
149
+ } catch {
150
+ ariaLabel = '';
151
+ ariaLabelledBy = '';
152
+ title = '';
153
+ }
164
154
 
165
- const details = {
166
- ariaLabel: ariaLabel || null,
167
- ariaLabelledBy: ariaLabelledBy || null,
168
- ariaLabelledByText: labelledByText || null,
169
- title: title || null
170
- };
155
+ // Only resolve IDREF text if aria-labelledby is present and aria-label is not already sufficient
156
+ let labelledByText = '';
157
+ let hasLabelledByMechanism = false;
171
158
 
172
- applicableCount += 1;
173
-
174
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, {targetSet: 'acc'}) : null;
175
-
176
- const baseOccurrence = {
177
- summary: 'Review text alternative for <embed> for accuracy and appropriateness.',
178
- hint: 'Confirm the ARIA name or title accurately identifies the embedded content in context.',
179
- i18n: {
180
- summaryKey: 'embed_textAltQuality_summary_cantTell',
181
- hintKey: 'embed_textAltQuality_hint_cantTell',
182
- params: {element: (el.tagName || '').toLowerCase()}
183
- },
184
- data: {
185
- visibilityFilter: eligInfo || {targetSet: 'acc', accEligible: null, reasons: []},
186
- details
187
- }
188
- };
159
+ if (!ariaLabel && ariaLabelledBy) {
160
+ hasLabelledByMechanism = true;
189
161
 
190
- if (helpers && typeof helpers.reportOccurrence === 'function') {
191
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
192
- } else {
193
- occurrences.push({selector: '', html: '', ...baseOccurrence});
162
+ if (getTextFromIdRefs) {
163
+ try {
164
+ const t = getTextFromIdRefs(ariaLabelledBy, ctx);
165
+ labelledByText = trim(t && t.text);
166
+ // If it resolves to empty, still treat as a mechanism present for manual review.
167
+ } catch {
168
+ labelledByText = '';
194
169
  }
170
+ }
195
171
  }
196
172
 
197
- if (applicableCount === 0) {
198
- return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
173
+ const hasNameMechanism = !!(ariaLabel || title || hasLabelledByMechanism);
174
+ if (!hasNameMechanism) continue;
175
+
176
+ const details = {
177
+ ariaLabel: ariaLabel || null,
178
+ ariaLabelledBy: ariaLabelledBy || null,
179
+ ariaLabelledByText: labelledByText || null,
180
+ title: title || null
181
+ };
182
+
183
+ applicableCount += 1;
184
+
185
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
186
+
187
+ const baseOccurrence = {
188
+ summary: 'Review text alternative for <embed> for accuracy and appropriateness.',
189
+ hint: 'Confirm the ARIA name or title accurately identifies the embedded content in context.',
190
+ i18n: {
191
+ summaryKey: 'embed_textAltQuality_summary_cantTell',
192
+ hintKey: 'embed_textAltQuality_hint_cantTell',
193
+ params: { element: (el.tagName || '').toLowerCase() }
194
+ },
195
+ data: {
196
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
197
+ details
198
+ }
199
+ };
200
+
201
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
202
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
203
+ } else {
204
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
199
205
  }
206
+ }
207
+
208
+ if (applicableCount === 0) {
209
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
210
+ }
200
211
 
201
- return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
212
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
202
213
  }
203
214
 
204
- module.exports = {id, meta, runInPage};
215
+ module.exports = { id, meta, runInPage };
@@ -42,7 +42,8 @@ const id = 'empty-heading';
42
42
 
43
43
  const meta = {
44
44
  title: 'Headings must not be empty',
45
- description: 'Checks that heading elements (<h1>-<h6> or role="heading") have a non-empty accessible name.',
45
+ description:
46
+ 'Checks that heading elements (<h1>-<h6> or role="heading") have a non-empty accessible name.',
46
47
  i18n: {
47
48
  titleKey: 'emptyHeading_title',
48
49
  descriptionKey: 'emptyHeading_description'
@@ -62,7 +63,9 @@ function runInPage(ctx) {
62
63
  const { document, helpers, rule } = ctx;
63
64
 
64
65
  function normalizeWs(s) {
65
- return String(s || '').replace(/\s+/g, ' ').trim();
66
+ return String(s || '')
67
+ .replace(/\s+/g, ' ')
68
+ .trim();
66
69
  }
67
70
 
68
71
  function getExplicitRoleToken(el) {
@@ -112,13 +115,16 @@ function runInPage(ctx) {
112
115
  return normalizeWs(el.getAttribute && el.getAttribute('title'));
113
116
  }
114
117
 
115
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('h1, h2, h3, h4, h5, h6, [role]') : helpers.queryAll('h1, h2, h3, h4, h5, h6, [role]');
118
+ const nodes = helpers.queryAllSmart
119
+ ? helpers.queryAllSmart('h1, h2, h3, h4, h5, h6, [role]')
120
+ : helpers.queryAll('h1, h2, h3, h4, h5, h6, [role]');
116
121
 
117
122
  const occurrences = [];
118
123
  let applicableCount = 0;
119
124
  const seen = new Set();
120
125
 
121
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
126
+ const isAccTreeEligible =
127
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
122
128
 
123
129
  function isEligible(el) {
124
130
  if (!isAccTreeEligible) return true;
@@ -143,9 +149,15 @@ function runInPage(ctx) {
143
149
  if (name) continue;
144
150
 
145
151
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
146
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
152
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
147
153
  const eligInfo = helpers.getEligibilityInfo
148
- ? (() => { try { return helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
154
+ ? (() => {
155
+ try {
156
+ return helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' });
157
+ } catch {
158
+ return null;
159
+ }
160
+ })()
149
161
  : null;
150
162
 
151
163
  occurrences.push({
@@ -169,7 +181,12 @@ function runInPage(ctx) {
169
181
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
170
182
  }
171
183
  if (occurrences.length) {
172
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
184
+ return {
185
+ ruleId: rule.ruleId,
186
+ outcome: 'cantTell',
187
+ severity: rule.defaultSeverity || 'minor',
188
+ occurrences
189
+ };
173
190
  }
174
191
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
175
192
  }
@@ -46,7 +46,8 @@ const id = 'empty-table-header';
46
46
 
47
47
  const meta = {
48
48
  title: 'Table header cells must not be empty',
49
- description: 'Checks that table header cells (<th>, or any element with role="columnheader"/"rowheader") have visible text content — a header named only via aria-label/aria-labelledby is also flagged, since real screen-reader/browser support for that is inconsistent.',
49
+ description:
50
+ 'Checks that table header cells (<th>, or any element with role="columnheader"/"rowheader") have visible text content — a header named only via aria-label/aria-labelledby is also flagged, since real screen-reader/browser support for that is inconsistent.',
50
51
  i18n: {
51
52
  titleKey: 'emptyTableHeader_title',
52
53
  descriptionKey: 'emptyTableHeader_description'
@@ -66,7 +67,9 @@ function runInPage(ctx) {
66
67
  const { document, helpers, rule } = ctx;
67
68
 
68
69
  function normalizeWs(s) {
69
- return String(s || '').replace(/\s+/g, ' ').trim();
70
+ return String(s || '')
71
+ .replace(/\s+/g, ' ')
72
+ .trim();
70
73
  }
71
74
 
72
75
  function getVisibleText(el) {
@@ -98,7 +101,9 @@ function runInPage(ctx) {
98
101
  // no conflicting explicit role, plus any element carrying an explicit
99
102
  // columnheader/rowheader role (native or not).
100
103
  const selector = 'th:not([role]), [role="columnheader"], [role="rowheader"]';
101
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
104
+ const nodes = helpers.queryAllSmart
105
+ ? helpers.queryAllSmart(selector)
106
+ : helpers.queryAll(selector);
102
107
 
103
108
  const occurrences = [];
104
109
  let applicableCount = 0;
@@ -110,14 +115,15 @@ function runInPage(ctx) {
110
115
  if (getVisibleText(el)) continue;
111
116
 
112
117
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
113
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
118
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
114
119
 
115
120
  const ariaName = getAriaOnlyName(el);
116
121
  if (ariaName) {
117
122
  occurrences.push({
118
123
  selector: stableSelector,
119
124
  html,
120
- summary: 'This table header cell has no visible text — its only accessible name comes from aria-label/aria-labelledby, which real screen-reader/browser combinations (e.g. NVDA+Firefox, iOS VoiceOver+Safari) are known to ignore on <th> elements.',
125
+ summary:
126
+ 'This table header cell has no visible text — its only accessible name comes from aria-label/aria-labelledby, which real screen-reader/browser combinations (e.g. NVDA+Firefox, iOS VoiceOver+Safari) are known to ignore on <th> elements.',
121
127
  hint: 'Add visible text content to this header cell (in addition to, or instead of, aria-label/aria-labelledby) — visible text is the only naming mechanism confirmed to work across tested screen readers.',
122
128
  i18n: {
123
129
  summaryKey: 'emptyTableHeader_summary_cantTell_ariaOnly',
@@ -151,7 +157,12 @@ function runInPage(ctx) {
151
157
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
152
158
  }
153
159
  if (occurrences.length) {
154
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
160
+ return {
161
+ ruleId: rule.ruleId,
162
+ outcome: 'cantTell',
163
+ severity: rule.defaultSeverity || 'minor',
164
+ occurrences
165
+ };
155
166
  }
156
167
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
157
168
  }
@@ -31,7 +31,8 @@ const id = 'focus-order-semantics';
31
31
 
32
32
  const meta = {
33
33
  title: 'Elements added to the tab order should have interactive semantics',
34
- description: 'Flags elements with tabindex >= 0 whose explicit role is a non-interactive structural/document role (e.g. heading, list, region, presentation), for manual review.',
34
+ description:
35
+ 'Flags elements with tabindex >= 0 whose explicit role is a non-interactive structural/document role (e.g. heading, list, region, presentation), for manual review.',
35
36
  i18n: {
36
37
  titleKey: 'focusOrderSemantics_title',
37
38
  descriptionKey: 'focusOrderSemantics_description'
@@ -48,17 +49,46 @@ const meta = {
48
49
  };
49
50
 
50
51
  function runInPage(ctx) {
51
- const { document, helpers, rule } = ctx;
52
+ const { helpers, rule } = ctx;
52
53
 
53
54
  const NON_INTERACTIVE_ROLES = new Set([
54
- 'generic', 'group', 'text', 'presentation', 'none', 'img', 'heading',
55
- 'region', 'article', 'banner', 'contentinfo', 'main', 'navigation',
56
- 'complementary', 'document', 'note', 'list', 'listitem', 'table',
57
- 'row', 'cell', 'columnheader', 'rowheader', 'figure', 'term',
58
- 'definition', 'paragraph', 'caption', 'status', 'alert', 'log', 'tooltip'
55
+ 'generic',
56
+ 'group',
57
+ 'text',
58
+ 'presentation',
59
+ 'none',
60
+ 'img',
61
+ 'heading',
62
+ 'region',
63
+ 'article',
64
+ 'banner',
65
+ 'contentinfo',
66
+ 'main',
67
+ 'navigation',
68
+ 'complementary',
69
+ 'document',
70
+ 'note',
71
+ 'list',
72
+ 'listitem',
73
+ 'table',
74
+ 'row',
75
+ 'cell',
76
+ 'columnheader',
77
+ 'rowheader',
78
+ 'figure',
79
+ 'term',
80
+ 'definition',
81
+ 'paragraph',
82
+ 'caption',
83
+ 'status',
84
+ 'alert',
85
+ 'log',
86
+ 'tooltip'
59
87
  ]);
60
88
 
61
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex][role]') : helpers.queryAll('[tabindex][role]');
89
+ const nodes = helpers.queryAllSmart
90
+ ? helpers.queryAllSmart('[tabindex][role]')
91
+ : helpers.queryAll('[tabindex][role]');
62
92
 
63
93
  const occurrences = [];
64
94
  let applicableCount = 0;
@@ -78,7 +108,7 @@ function runInPage(ctx) {
78
108
  if (!NON_INTERACTIVE_ROLES.has(role)) continue;
79
109
 
80
110
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
81
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
111
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
82
112
 
83
113
  const baseOccurrence = {
84
114
  selector: stableSelector,
@@ -107,7 +137,12 @@ function runInPage(ctx) {
107
137
  }
108
138
 
109
139
  if (occurrences.length) {
110
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
140
+ return {
141
+ ruleId: rule.ruleId,
142
+ outcome: 'cantTell',
143
+ severity: rule.defaultSeverity || 'minor',
144
+ occurrences
145
+ };
111
146
  }
112
147
 
113
148
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };