@surea11y/core 1.1.2 → 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 (160) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/LICENSE +373 -21
  3. package/README.md +70 -1
  4. package/bin/core.js +240 -11
  5. package/docs/API_STABILITY.md +61 -0
  6. package/docs/BASELINE.md +66 -0
  7. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  8. package/docs/CI_INTEGRATIONS.md +103 -0
  9. package/docs/CLI.md +80 -1
  10. package/docs/ENGINE_OPTIONS.md +4 -0
  11. package/docs/INTEGRATION.md +19 -1
  12. package/docs/OUTPUT_SCHEMA.md +2 -2
  13. package/docs/REPORT.md +33 -0
  14. package/docs/RULE_AUTHORING.md +31 -0
  15. package/docs/RULE_CATALOG.md +1 -1
  16. package/docs/SARIF.md +59 -0
  17. package/package.json +15 -4
  18. package/src/baseline.js +0 -0
  19. package/src/catalogs/composites.wcag.js +414 -450
  20. package/src/checks/automatic/area-alt-present.js +59 -25
  21. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  22. package/src/checks/automatic/aria-allowed-role.js +21 -7
  23. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  24. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  25. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  26. package/src/checks/automatic/aria-hidden-body.js +50 -19
  27. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  28. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  29. package/src/checks/automatic/aria-prohibited-children.js +125 -29
  30. package/src/checks/automatic/aria-required-attr.js +23 -8
  31. package/src/checks/automatic/aria-required-children.js +37 -14
  32. package/src/checks/automatic/aria-required-parent.js +48 -14
  33. package/src/checks/automatic/aria-role-name-present.js +47 -21
  34. package/src/checks/automatic/aria-roles-valid.js +22 -12
  35. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  36. package/src/checks/automatic/aria-valid-attr.js +17 -5
  37. package/src/checks/automatic/autocomplete-valid.js +74 -16
  38. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  39. package/src/checks/automatic/binary-control-name-present.js +60 -50
  40. package/src/checks/automatic/button-name-present.js +48 -18
  41. package/src/checks/automatic/bypass-blocks-present.js +50 -25
  42. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  43. package/src/checks/automatic/combobox-name-present.js +38 -45
  44. package/src/checks/automatic/contrast-computable.js +361 -341
  45. package/src/checks/automatic/contrast-enhanced.js +487 -466
  46. package/src/checks/automatic/contrast-minimum.js +486 -465
  47. package/src/checks/automatic/css-orientation-lock.js +39 -9
  48. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  49. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  50. package/src/checks/automatic/dialog-name-present.js +37 -75
  51. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  52. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  53. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  55. package/src/checks/automatic/form-control-single-label.js +47 -10
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
  57. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  58. package/src/checks/automatic/iframe-name-present.js +31 -9
  59. package/src/checks/automatic/iframe-title-unique.js +29 -8
  60. package/src/checks/automatic/img-alt-present.js +47 -43
  61. package/src/checks/automatic/input-image-alt-present.js +143 -112
  62. package/src/checks/automatic/label-in-name.js +50 -22
  63. package/src/checks/automatic/language-page-present.js +117 -109
  64. package/src/checks/automatic/link-in-text-block.js +59 -19
  65. package/src/checks/automatic/link-name-present.js +45 -14
  66. package/src/checks/automatic/list-children-valid.js +26 -9
  67. package/src/checks/automatic/listbox-name-present.js +39 -19
  68. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  69. package/src/checks/automatic/menuitem-name-present.js +39 -61
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
  73. package/src/checks/automatic/meter-name-present.js +36 -33
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  75. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  76. package/src/checks/automatic/option-name-present.js +38 -21
  77. package/src/checks/automatic/page-title-present.js +26 -7
  78. package/src/checks/automatic/progressbar-name-present.js +41 -34
  79. package/src/checks/automatic/role-img-alt-present.js +209 -157
  80. package/src/checks/automatic/searchbox-name-present.js +39 -19
  81. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  82. package/src/checks/automatic/slider-name-present.js +40 -47
  83. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  84. package/src/checks/automatic/summary-name-present.js +37 -17
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  86. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  87. package/src/checks/automatic/tab-name-present.js +37 -60
  88. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  89. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  90. package/src/checks/automatic/target-size-minimum.js +118 -48
  91. package/src/checks/automatic/td-has-header.js +29 -11
  92. package/src/checks/automatic/textbox-name-present.js +39 -19
  93. package/src/checks/automatic/tooltip-name-present.js +37 -18
  94. package/src/checks/automatic/treeitem-name-present.js +38 -21
  95. package/src/checks/automatic/valid-lang.js +20 -6
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  97. package/src/checks/manual/accesskeys-manual.js +14 -5
  98. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  99. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  101. package/src/checks/manual/aria-text-manual.js +14 -6
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  103. package/src/checks/manual/css-hidden-focus.js +196 -165
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  105. package/src/checks/manual/empty-heading-manual.js +24 -12
  106. package/src/checks/manual/empty-table-header-manual.js +21 -14
  107. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  109. package/src/checks/manual/heading-order-manual.js +22 -12
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  112. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  113. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  115. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  116. package/src/checks/manual/label-title-only-manual.js +14 -5
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  123. package/src/checks/manual/landmark-one-main-manual.js +35 -21
  124. package/src/checks/manual/landmark-unique-manual.js +73 -45
  125. package/src/checks/manual/link-name-quality-manual.js +43 -12
  126. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  127. package/src/checks/manual/meta-viewport-large-manual.js +25 -6
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  129. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  131. package/src/checks/manual/p-as-heading-manual.js +22 -7
  132. package/src/checks/manual/page-has-heading-one-manual.js +40 -23
  133. package/src/checks/manual/page-title-patterns-manual.js +87 -51
  134. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  135. package/src/checks/manual/region-manual.js +275 -62
  136. package/src/checks/manual/scope-attr-valid-manual.js +11 -9
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  138. package/src/checks/manual/skip-link-manual.js +35 -12
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  140. package/src/checks/manual/tabindex-manual.js +11 -9
  141. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  142. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  143. package/src/checks/manual/video-caption-manual.js +15 -4
  144. package/src/checks/manual-review.js +56 -12
  145. package/src/core/aria-helpers.js +1128 -823
  146. package/src/core/contrast-helpers.js +1217 -1062
  147. package/src/core/dom-helpers.js +4176 -3886
  148. package/src/core/dom-runner.js +720 -596
  149. package/src/core/frame-messaging.js +189 -138
  150. package/src/core/frame-scan.js +94 -82
  151. package/src/core/rollup-composites.js +94 -102
  152. package/src/core/rule-meta.js +71 -35
  153. package/src/core.js +37939 -29121
  154. package/src/i18n/en.js +1194 -887
  155. package/src/i18n/fr.js +1136 -793
  156. package/src/policy/contracts.js +13 -13
  157. package/src/policy/resolvePolicy.js +48 -44
  158. package/src/report.js +502 -0
  159. package/src/sarif.js +175 -0
  160. 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,18 +115,16 @@ function runInPage(ctx) {
112
115
  return normalizeWs(el.getAttribute && el.getAttribute('title'));
113
116
  }
114
117
 
115
- let nodes = [];
116
- try {
117
- nodes = document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role]');
118
- } catch {
119
- nodes = [];
120
- }
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]');
121
121
 
122
122
  const occurrences = [];
123
123
  let applicableCount = 0;
124
124
  const seen = new Set();
125
125
 
126
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
126
+ const isAccTreeEligible =
127
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
127
128
 
128
129
  function isEligible(el) {
129
130
  if (!isAccTreeEligible) return true;
@@ -148,9 +149,15 @@ function runInPage(ctx) {
148
149
  if (name) continue;
149
150
 
150
151
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
151
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
152
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
152
153
  const eligInfo = helpers.getEligibilityInfo
153
- ? (() => { 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
+ })()
154
161
  : null;
155
162
 
156
163
  occurrences.push({
@@ -174,7 +181,12 @@ function runInPage(ctx) {
174
181
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
175
182
  }
176
183
  if (occurrences.length) {
177
- 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
+ };
178
190
  }
179
191
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
180
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) {
@@ -94,15 +97,13 @@ function runInPage(ctx) {
94
97
  return '';
95
98
  }
96
99
 
97
- let nodes = [];
98
- try {
99
- // Matches a widely-used reference engine's own empty-table-header selector exactly: a <th> with
100
- // no conflicting explicit role, plus any element carrying an explicit
101
- // columnheader/rowheader role (native or not).
102
- nodes = document.querySelectorAll('th:not([role]), [role="columnheader"], [role="rowheader"]');
103
- } catch {
104
- nodes = [];
105
- }
100
+ // Matches a widely-used reference engine's own empty-table-header selector exactly: a <th> with
101
+ // no conflicting explicit role, plus any element carrying an explicit
102
+ // columnheader/rowheader role (native or not).
103
+ const selector = 'th:not([role]), [role="columnheader"], [role="rowheader"]';
104
+ const nodes = helpers.queryAllSmart
105
+ ? helpers.queryAllSmart(selector)
106
+ : helpers.queryAll(selector);
106
107
 
107
108
  const occurrences = [];
108
109
  let applicableCount = 0;
@@ -114,14 +115,15 @@ function runInPage(ctx) {
114
115
  if (getVisibleText(el)) continue;
115
116
 
116
117
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
117
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
118
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
118
119
 
119
120
  const ariaName = getAriaOnlyName(el);
120
121
  if (ariaName) {
121
122
  occurrences.push({
122
123
  selector: stableSelector,
123
124
  html,
124
- 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.',
125
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.',
126
128
  i18n: {
127
129
  summaryKey: 'emptyTableHeader_summary_cantTell_ariaOnly',
@@ -155,7 +157,12 @@ function runInPage(ctx) {
155
157
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
156
158
  }
157
159
  if (occurrences.length) {
158
- 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
+ };
159
166
  }
160
167
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
161
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: [] };