@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,199 +11,240 @@
11
11
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
12
  */
13
13
 
14
- const id = "svg-text-alternative-quality";
14
+ const id = 'svg-text-alternative-quality';
15
15
 
16
16
  const meta = {
17
- title: "<svg> text alternative must be appropriate (manual review)",
18
- description: "Flags applicable <svg> graphics with a detected text alternative for human review of appropriateness.",
19
- i18n: {
20
- titleKey: "svg_textAltQuality_title",
21
- descriptionKey: "svg_textAltQuality_description"
22
- },
23
- helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "nontext", "svg", "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: '<svg> text alternative must be appropriate (manual review)',
18
+ description:
19
+ 'Flags applicable <svg> graphics with a detected text alternative for human review of appropriateness.',
20
+ i18n: {
21
+ titleKey: 'svg_textAltQuality_title',
22
+ descriptionKey: 'svg_textAltQuality_description'
23
+ },
24
+ helpUrl: null,
25
+ tags: ['wcag2a', 'wcag111', 'nontext', 'svg', 'manual', 'atomic'],
26
+ wcagSc: ['1.1.1'],
27
+ normativeMappings: [
28
+ {
29
+ standard: 'WCAG',
30
+ version: '2.2',
31
+ requirement: '1.1.1',
32
+ title: 'Non-text Content',
33
+ conformanceLevel: 'A'
34
+ }
35
+ ],
36
+ defaultSeverity: 'minor',
37
+ category: 'perceivable',
38
+ type: 'manual',
39
+ defaultConfidence: 'medium',
40
+ coverage: {
41
+ facetsBySc: {
42
+ '1.1.1': ['text-alternative-quality']
37
43
  }
44
+ }
38
45
  };
39
46
 
40
47
  function runInPage(ctx) {
41
- const {document, root, helpers, rule} = ctx;
42
- const safeRoot = root || document;
43
-
44
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
45
- const queryAll = helpers && typeof helpers.queryAll === 'function'
46
- ? helpers.queryAll
47
- : (sel) => {
48
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
49
- catch { return []; }
48
+ const { document, root, helpers, rule } = ctx;
49
+ const safeRoot = root || document;
50
+
51
+ const queryAllSmart =
52
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
+ const queryAll =
54
+ helpers && typeof helpers.queryAll === 'function'
55
+ ? helpers.queryAll
56
+ : (sel) => {
57
+ try {
58
+ return safeRoot && safeRoot.querySelectorAll
59
+ ? Array.from(safeRoot.querySelectorAll(sel))
60
+ : [];
61
+ } catch {
62
+ return [];
63
+ }
50
64
  };
51
65
 
52
- const buildSelector = helpers && typeof helpers.buildSelector === 'function'
53
- ? helpers.buildSelector
54
- : (el) => {
55
- try {
56
- if (!el || !el.tagName) return 'html';
57
- const tag = (el.tagName || 'html').toLowerCase();
58
- return el.id ? `${tag}#${el.id}` : tag;
59
- } catch {
60
- return 'html';
61
- }
66
+ const buildSelector =
67
+ helpers && typeof helpers.buildSelector === 'function'
68
+ ? helpers.buildSelector
69
+ : (el) => {
70
+ try {
71
+ if (!el || !el.tagName) return 'html';
72
+ const tag = (el.tagName || 'html').toLowerCase();
73
+ return el.id ? `${tag}#${el.id}` : tag;
74
+ } catch {
75
+ return 'html';
76
+ }
62
77
  };
63
78
 
64
- const getOuterHtmlSnippet = helpers && typeof helpers.getOuterHtmlSnippet === 'function'
65
- ? helpers.getOuterHtmlSnippet
66
- : (el) => { try { return (el && el.outerHTML) ? String(el.outerHTML).slice(0, 2000) : ''; } catch { return ''; } };
67
-
68
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
69
- ? helpers.getEligibilityInfo
70
- : null;
71
-
72
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
73
- ? helpers.isAccTreeEligible
74
- : null;
75
-
76
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
77
- ? helpers.getFocusableInfo
78
- : null;
79
-
80
- function isRolePresentationExcluded(el) {
81
- const role = (() => {
82
- try {
83
- return String(el.getAttribute('role') || '').trim().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 = false;
92
- if (getFocusableInfo) {
93
- const fi = (() => {
94
- try {
95
- return getFocusableInfo(el, ctx);
96
- } catch {
97
- return null;
98
- }
99
- })();
100
- focusable = !!(fi && fi.focusable);
101
- } else {
102
- const tabindex = el.getAttribute('tabindex');
103
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
104
- }
105
- return !focusable;
106
- }
79
+ const getOuterHtmlSnippet =
80
+ helpers && typeof helpers.getOuterHtmlSnippet === 'function'
81
+ ? helpers.getOuterHtmlSnippet
82
+ : (el) => {
83
+ try {
84
+ return el && el.outerHTML ? String(el.outerHTML).slice(0, 2000) : '';
85
+ } catch {
86
+ return '';
87
+ }
88
+ };
89
+
90
+ const getEligibilityInfo =
91
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
107
92
 
93
+ const isAccTreeEligible =
94
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
108
95
 
109
- const els = (() => {
96
+ const getFocusableInfo =
97
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
98
+
99
+ function isRolePresentationExcluded(el) {
100
+ const role = (() => {
101
+ try {
102
+ return String(el.getAttribute('role') || '')
103
+ .trim()
104
+ .toLowerCase();
105
+ } catch {
106
+ return '';
107
+ }
108
+ })();
109
+ if (role !== 'presentation' && role !== 'none') return false;
110
+
111
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
112
+ let focusable;
113
+ if (getFocusableInfo) {
114
+ const fi = (() => {
110
115
  try {
111
- return Array.from((queryAllSmart ? queryAllSmart('svg') : queryAll('svg')) || []);
116
+ return getFocusableInfo(el, ctx);
112
117
  } catch {
113
- return queryAll('svg');
118
+ return null;
114
119
  }
115
- })();
116
-
117
- if (!els.length) {
118
- return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
120
+ })();
121
+ focusable = !!(fi && fi.focusable);
122
+ } else {
123
+ const tabindex = el.getAttribute('tabindex');
124
+ focusable =
125
+ tabindex != null &&
126
+ String(tabindex).trim() !== '' &&
127
+ !Number.isNaN(Number(String(tabindex).trim()));
119
128
  }
129
+ return !focusable;
130
+ }
131
+
132
+ const els = (() => {
133
+ try {
134
+ return Array.from((queryAllSmart ? queryAllSmart('svg') : queryAll('svg')) || []);
135
+ } catch {
136
+ return queryAll('svg');
137
+ }
138
+ })();
120
139
 
121
- const occurrences = [];
122
- let applicableCount = 0;
123
-
124
- const trim = (v) => (v == null ? '' : String(v)).trim();
125
-
126
- for (const el of els) {
127
- if (!el || !el.getAttribute) continue;
140
+ if (!els.length) {
141
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
142
+ }
128
143
 
129
- // acc eligibility
130
- if (isAccTreeEligible) {
131
- const elig = (() => { try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; } })();
132
- if (elig && elig.eligible === false) continue;
133
- }
144
+ const occurrences = [];
145
+ let applicableCount = 0;
134
146
 
135
- if (isRolePresentationExcluded(el)) continue;
147
+ const trim = (v) => (v == null ? '' : String(v)).trim();
136
148
 
137
- // Detect mechanisms once
138
- let titleText = '';
139
- let descText = '';
140
- let ariaLabel = '';
141
- let ariaLabelledBy = '';
142
- let labelledByText = '';
149
+ for (const el of els) {
150
+ if (!el || !el.getAttribute) continue;
143
151
 
152
+ // acc eligibility
153
+ if (isAccTreeEligible) {
154
+ const elig = (() => {
144
155
  try {
145
- const titleEl = el.querySelector ? el.querySelector('title') : null;
146
- const descEl = el.querySelector ? el.querySelector('desc') : null;
147
- titleText = trim(titleEl && titleEl.textContent);
148
- descText = trim(descEl && descEl.textContent);
149
-
150
- ariaLabel = trim(el.getAttribute('aria-label'));
151
- ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
152
- } catch {}
153
-
154
- if (!ariaLabel && ariaLabelledBy && helpers && typeof helpers.getTextFromIdRefs === 'function') {
155
- try {
156
- const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
157
- labelledByText = trim(t && t.text);
158
- } catch {}
156
+ return isAccTreeEligible(el, ctx);
157
+ } catch {
158
+ return { eligible: true, reasons: [] };
159
159
  }
160
+ })();
161
+ if (elig && elig.eligible === false) continue;
162
+ }
160
163
 
161
- const hasNonEmptyTitle = !!titleText;
162
- const hasNonEmptyDesc = !!descText;
163
- const hasAriaLabel = !!ariaLabel;
164
- const hasResolvedLabelledBy = !!labelledByText;
165
-
166
- const hasMechanism = hasNonEmptyTitle || hasNonEmptyDesc || hasAriaLabel || hasResolvedLabelledBy;
167
- if (!hasMechanism) continue;
168
-
169
- applicableCount += 1;
170
-
171
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
172
-
173
- const baseOccurrence = {
174
- summary: 'Review text alternative for <svg> for accuracy and appropriateness.',
175
- hint: 'Confirm the <title>/<desc> or ARIA name conveys the meaning/purpose of the graphic in context.',
176
- i18n: {
177
- summaryKey: 'svg_textAltQuality_summary_cantTell',
178
- hintKey: 'svg_textAltQuality_hint_cantTell',
179
- params: { element: 'svg' }
180
- },
181
- data: {
182
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
183
- details: {
184
- hasNonEmptyTitle,
185
- hasNonEmptyDesc,
186
- ariaLabel: ariaLabel || null,
187
- ariaLabelledBy: ariaLabelledBy || null,
188
- ariaLabelledByText: labelledByText ? labelledByText.slice(0, 120) : null // deterministic truncation
189
- }
190
- }
191
- };
164
+ if (isRolePresentationExcluded(el)) continue;
165
+
166
+ // Detect mechanisms once
167
+ let titleText = '';
168
+ let descText = '';
169
+ let ariaLabel = '';
170
+ let ariaLabelledBy = '';
171
+ let labelledByText = '';
172
+
173
+ try {
174
+ const titleEl = el.querySelector ? el.querySelector('title') : null;
175
+ const descEl = el.querySelector ? el.querySelector('desc') : null;
176
+ titleText = trim(titleEl && titleEl.textContent);
177
+ descText = trim(descEl && descEl.textContent);
178
+
179
+ ariaLabel = trim(el.getAttribute('aria-label'));
180
+ ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
181
+ } catch {}
182
+
183
+ if (
184
+ !ariaLabel &&
185
+ ariaLabelledBy &&
186
+ helpers &&
187
+ typeof helpers.getTextFromIdRefs === 'function'
188
+ ) {
189
+ try {
190
+ const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
191
+ labelledByText = trim(t && t.text);
192
+ } catch {}
193
+ }
192
194
 
193
- if (helpers && typeof helpers.reportOccurrence === 'function') {
194
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
195
- } else {
196
- const selectorStr = (() => { try { return buildSelector(el); } catch { return 'html'; } })();
197
- const html = getOuterHtmlSnippet(el);
198
- occurrences.push({ selector: selectorStr, html, ...baseOccurrence });
195
+ const hasNonEmptyTitle = !!titleText;
196
+ const hasNonEmptyDesc = !!descText;
197
+ const hasAriaLabel = !!ariaLabel;
198
+ const hasResolvedLabelledBy = !!labelledByText;
199
+
200
+ const hasMechanism =
201
+ hasNonEmptyTitle || hasNonEmptyDesc || hasAriaLabel || hasResolvedLabelledBy;
202
+ if (!hasMechanism) continue;
203
+
204
+ applicableCount += 1;
205
+
206
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
207
+
208
+ const baseOccurrence = {
209
+ summary: 'Review text alternative for <svg> for accuracy and appropriateness.',
210
+ hint: 'Confirm the <title>/<desc> or ARIA name conveys the meaning/purpose of the graphic in context.',
211
+ i18n: {
212
+ summaryKey: 'svg_textAltQuality_summary_cantTell',
213
+ hintKey: 'svg_textAltQuality_hint_cantTell',
214
+ params: { element: 'svg' }
215
+ },
216
+ data: {
217
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
218
+ details: {
219
+ hasNonEmptyTitle,
220
+ hasNonEmptyDesc,
221
+ ariaLabel: ariaLabel || null,
222
+ ariaLabelledBy: ariaLabelledBy || null,
223
+ ariaLabelledByText: labelledByText ? labelledByText.slice(0, 120) : null // deterministic truncation
199
224
  }
200
- }
225
+ }
226
+ };
201
227
 
202
- if (applicableCount === 0) {
203
- return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
228
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
229
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
230
+ } else {
231
+ const selectorStr = (() => {
232
+ try {
233
+ return buildSelector(el);
234
+ } catch {
235
+ return 'html';
236
+ }
237
+ })();
238
+ const html = getOuterHtmlSnippet(el);
239
+ occurrences.push({ selector: selectorStr, html, ...baseOccurrence });
204
240
  }
241
+ }
242
+
243
+ if (applicableCount === 0) {
244
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
245
+ }
205
246
 
206
- return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
247
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
207
248
  }
208
249
 
209
- module.exports = {id, meta, runInPage};
250
+ module.exports = { id, meta, runInPage };
@@ -42,7 +42,9 @@ const meta = {
42
42
  function runInPage(ctx) {
43
43
  const { helpers, rule } = ctx;
44
44
 
45
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex]') : helpers.queryAll('[tabindex]');
45
+ const nodes = helpers.queryAllSmart
46
+ ? helpers.queryAllSmart('[tabindex]')
47
+ : helpers.queryAll('[tabindex]');
46
48
 
47
49
  const occurrences = [];
48
50
  let applicableCount = 0;
@@ -59,7 +61,7 @@ function runInPage(ctx) {
59
61
  if (n <= 0) continue;
60
62
 
61
63
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
62
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
64
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
63
65
 
64
66
  occurrences.push({
65
67
  selector: stableSelector,
@@ -81,7 +83,12 @@ function runInPage(ctx) {
81
83
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
82
84
  }
83
85
  if (occurrences.length) {
84
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
86
+ return {
87
+ ruleId: rule.ruleId,
88
+ outcome: 'cantTell',
89
+ severity: rule.defaultSeverity || 'minor',
90
+ occurrences
91
+ };
85
92
  }
86
93
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
87
94
  }
@@ -26,7 +26,8 @@ const id = 'table-duplicate-name';
26
26
 
27
27
  const meta = {
28
28
  title: 'Table caption must not duplicate its summary attribute',
29
- description: 'Checks that a <table>\'s <caption> text is not identical to its (deprecated) summary attribute.',
29
+ description:
30
+ "Checks that a <table>'s <caption> text is not identical to its (deprecated) summary attribute.",
30
31
  i18n: {
31
32
  titleKey: 'tableDuplicateName_title',
32
33
  descriptionKey: 'tableDuplicateName_description'
@@ -43,13 +44,17 @@ const meta = {
43
44
  };
44
45
 
45
46
  function runInPage(ctx) {
46
- const { document, helpers, rule } = ctx;
47
+ const { helpers, rule } = ctx;
47
48
 
48
49
  function normalizeWs(s) {
49
- return String(s || '').replace(/\s+/g, ' ').trim();
50
+ return String(s || '')
51
+ .replace(/\s+/g, ' ')
52
+ .trim();
50
53
  }
51
54
 
52
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]') : helpers.queryAll('table[summary]');
55
+ const nodes = helpers.queryAllSmart
56
+ ? helpers.queryAllSmart('table[summary]')
57
+ : helpers.queryAll('table[summary]');
53
58
 
54
59
  const occurrences = [];
55
60
  let applicableCount = 0;
@@ -68,12 +73,12 @@ function runInPage(ctx) {
68
73
  if (captionText.toLowerCase() !== summary.toLowerCase()) continue;
69
74
 
70
75
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
71
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
76
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
72
77
 
73
78
  occurrences.push({
74
79
  selector: stableSelector,
75
80
  html,
76
- summary: 'This table\'s caption duplicates its summary attribute.',
81
+ summary: "This table's caption duplicates its summary attribute.",
77
82
  hint: 'Remove the redundant summary attribute, or make it provide different information than the caption.',
78
83
  i18n: {
79
84
  summaryKey: 'tableDuplicateName_summary_cantTell',
@@ -90,7 +95,12 @@ function runInPage(ctx) {
90
95
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
91
96
  }
92
97
  if (occurrences.length) {
93
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
98
+ return {
99
+ ruleId: rule.ruleId,
100
+ outcome: 'cantTell',
101
+ severity: rule.defaultSeverity || 'minor',
102
+ occurrences
103
+ };
94
104
  }
95
105
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
96
106
  }
@@ -32,7 +32,8 @@ const id = 'table-fake-caption';
32
32
 
33
33
  const meta = {
34
34
  title: "A table's first row should not stand in for a real <caption>",
35
- description: 'Flags tables with no <caption> whose first row has a single non-empty cell while other rows have multiple cells, for manual review of whether that cell is acting as a fake caption.',
35
+ description:
36
+ 'Flags tables with no <caption> whose first row has a single non-empty cell while other rows have multiple cells, for manual review of whether that cell is acting as a fake caption.',
36
37
  i18n: {
37
38
  titleKey: 'tableFakeCaption_title',
38
39
  descriptionKey: 'tableFakeCaption_description'
@@ -41,7 +42,13 @@ const meta = {
41
42
  tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'manual'],
42
43
  wcagSc: ['1.3.1'],
43
44
  normativeMappings: [
44
- { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
45
+ {
46
+ standard: 'WCAG',
47
+ version: '2.2',
48
+ requirement: '1.3.1',
49
+ title: 'Info and Relationships',
50
+ conformanceLevel: 'A'
51
+ }
45
52
  ],
46
53
  defaultSeverity: 'minor',
47
54
  category: 'perceivable',
@@ -51,9 +58,11 @@ const meta = {
51
58
  };
52
59
 
53
60
  function runInPage(ctx) {
54
- const { document, helpers, rule } = ctx;
61
+ const { helpers, rule } = ctx;
55
62
 
56
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
63
+ function trim(v) {
64
+ return (v == null ? '' : String(v)).trim();
65
+ }
57
66
 
58
67
  const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
59
68
 
@@ -83,12 +92,15 @@ function runInPage(ctx) {
83
92
  if (!hasMultiCellRow) continue;
84
93
 
85
94
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(table) : 'html';
86
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(table) : (table.outerHTML || '');
95
+ const html = helpers.getOuterHtmlSnippet
96
+ ? helpers.getOuterHtmlSnippet(table)
97
+ : table.outerHTML || '';
87
98
 
88
99
  const baseOccurrence = {
89
100
  selector: stableSelector,
90
101
  html,
91
- summary: 'This table has no <caption>, but its first row is a single cell sitting above multi-cell rows — it may be acting as a fake caption.',
102
+ summary:
103
+ 'This table has no <caption>, but its first row is a single cell sitting above multi-cell rows — it may be acting as a fake caption.',
92
104
  hint: 'If this cell is meant to describe the table, use a real <caption> element instead of a lone first-row cell.',
93
105
  i18n: {
94
106
  summaryKey: 'tableFakeCaption_summary_cantTell',
@@ -112,7 +124,12 @@ function runInPage(ctx) {
112
124
  }
113
125
 
114
126
  if (occurrences.length) {
115
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
127
+ return {
128
+ ruleId: rule.ruleId,
129
+ outcome: 'cantTell',
130
+ severity: rule.defaultSeverity || 'minor',
131
+ occurrences
132
+ };
116
133
  }
117
134
 
118
135
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -41,7 +41,13 @@ const meta = {
41
41
  tags: ['wcag2a', 'wcag122', 'media', 'timebasedmedia', 'atomic', 'manual'],
42
42
  wcagSc: ['1.2.2'],
43
43
  normativeMappings: [
44
- { standard: 'WCAG', version: '2.2', requirement: '1.2.2', title: 'Captions (Prerecorded)', conformanceLevel: 'A' }
44
+ {
45
+ standard: 'WCAG',
46
+ version: '2.2',
47
+ requirement: '1.2.2',
48
+ title: 'Captions (Prerecorded)',
49
+ conformanceLevel: 'A'
50
+ }
45
51
  ],
46
52
  defaultSeverity: 'moderate',
47
53
  category: 'perceivable',
@@ -51,7 +57,7 @@ const meta = {
51
57
  };
52
58
 
53
59
  function runInPage(ctx) {
54
- const { document, helpers, rule } = ctx;
60
+ const { helpers, rule } = ctx;
55
61
 
56
62
  const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video') : helpers.queryAll('video');
57
63
 
@@ -77,7 +83,7 @@ function runInPage(ctx) {
77
83
  if (hasCaptionsTrack) continue;
78
84
 
79
85
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
80
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
86
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
81
87
 
82
88
  const baseOccurrence = {
83
89
  selector: stableSelector,
@@ -106,7 +112,12 @@ function runInPage(ctx) {
106
112
  }
107
113
 
108
114
  if (occurrences.length) {
109
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'moderate', occurrences };
115
+ return {
116
+ ruleId: rule.ruleId,
117
+ outcome: 'cantTell',
118
+ severity: rule.defaultSeverity || 'moderate',
119
+ occurrences
120
+ };
110
121
  }
111
122
 
112
123
  // Manual rules may only emit cantTell/notApplicable (never pass/fail):