@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
@@ -13,7 +13,13 @@ const meta = {
13
13
  tags: ['wcag2a', 'wcag111', 'nontext', 'atomic', 'automatic', 'name'],
14
14
  wcagSc: ['1.1.1'],
15
15
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
16
+ {
17
+ standard: 'WCAG',
18
+ version: '2.2',
19
+ requirement: '1.1.1',
20
+ title: 'Non-text Content',
21
+ conformanceLevel: 'A'
22
+ }
17
23
  ],
18
24
  defaultSeverity: 'serious',
19
25
  category: 'perceivable',
@@ -24,36 +30,22 @@ const meta = {
24
30
 
25
31
  function runInPage(ctx) {
26
32
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
33
+ const getEligibilityInfo =
34
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
35
 
32
36
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
37
+ return String(s || '')
38
+ .replace(/\s+/g, ' ')
39
+ .trim();
34
40
  }
35
41
 
36
42
  function getAttr(el, name) {
37
43
  try {
38
44
  if (!el || !el.getAttribute) return '';
39
45
  return normalizeWs(el.getAttribute(name));
40
- } catch { return ''; }
41
- }
42
-
43
- function getConservativeSubtreeText(document, container) {
44
- // "Name from content" — recurses into descendants and uses each one's
45
- // own accessible name (img alt, aria-label/aria-labelledby, title) when
46
- // it has one, not just literal text nodes. See getContentNameInfo's
47
- // header comment in src/core/dom-helpers.js for the full rationale
48
- // (this replaced a text-node-only TreeWalker that missed the common
49
- // "<a><img alt='...'></a>" logo-link / "<button><img alt='...'></button>"
50
- // icon-button pattern).
51
- if (helpers.getContentNameInfo) {
52
- const info = helpers.getContentNameInfo(container, ctx);
53
- return info && info.present ? info.value : '';
46
+ } catch {
47
+ return '';
54
48
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
56
- return t.replace(/\s+/g, ' ').trim();
57
49
  }
58
50
 
59
51
  function resolveAriaLabelledbyText(document, el, maxRefs) {
@@ -74,7 +66,8 @@ function runInPage(ctx) {
74
66
  }
75
67
 
76
68
  function isEligibleAcc(helpers, el, ctx) {
77
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
69
+ const fn =
70
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
71
  if (!fn) return true;
79
72
  try {
80
73
  const r = fn(el, ctx);
@@ -85,13 +78,13 @@ function runInPage(ctx) {
85
78
  }
86
79
  }
87
80
 
88
-
89
81
  const occurrences = [];
90
82
  let applicableCount = 0;
91
83
 
92
84
  const selector = '[role="progressbar"]';
93
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
94
-
85
+ const nodes = helpers.queryAllSmart
86
+ ? helpers.queryAllSmart(selector)
87
+ : helpers.queryAll(selector);
95
88
 
96
89
  function evaluate(el) {
97
90
  const ariaLabel = getAttr(el, 'aria-label');
@@ -129,36 +122,50 @@ function runInPage(ctx) {
129
122
  if (res.ok) continue;
130
123
 
131
124
  const eligInfo = getEligibilityInfo
132
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
133
- : null;
125
+ ? (() => {
126
+ try {
127
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
128
+ } catch {
129
+ return null;
130
+ }
131
+ })()
132
+ : null;
134
133
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
135
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
134
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
136
135
 
137
136
  occurrences.push({
138
137
  selector: stableSelector,
139
138
  html,
140
139
  summary: 'This progress bar has no accessible name.',
141
- hint: 'Provide progress bar text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
140
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this progress bar's accessible name.",
142
141
  i18n: {
143
142
  summaryKey: 'progressbarNamePresent_summary_fail',
144
143
  hintKey: 'progressbarNamePresent_hint_fail',
145
144
  params: {}
146
145
  },
147
146
  data: {
148
- details: { reasonCode: 'name_missing', controlType: 'progressbar', methodTried: res.method },
147
+ details: {
148
+ reasonCode: 'name_missing',
149
+ controlType: 'progressbar',
150
+ methodTried: res.method
151
+ },
149
152
  visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
150
153
  }
151
154
  });
152
155
  }
153
156
 
154
-
155
157
  if (applicableCount === 0) {
156
158
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
157
159
  }
158
160
  if (occurrences.length) {
159
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
161
+ return {
162
+ ruleId: rule.ruleId,
163
+ outcome: 'fail',
164
+ severity: rule.defaultSeverity || 'minor',
165
+ occurrences
166
+ };
160
167
  }
161
168
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
162
169
  }
163
170
 
164
- module.exports = { id, meta, runInPage };
171
+ module.exports = { id, meta, runInPage };
@@ -21,186 +21,238 @@
21
21
  const id = 'role-img-text-alternative-present';
22
22
 
23
23
  const meta = {
24
- title: '[role="img"] must have an accessible text alternative',
25
- description:
26
- 'Checks that elements with role="img" provide an accessible text alternative via aria-label, aria-labelledby, or a title attribute.',
27
- i18n: {
28
- titleKey: 'roleImg_textAlternativePresent_title',
29
- descriptionKey: 'roleImg_textAlternativePresent_description'
30
- },
31
- helpUrl: null,
32
- tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'aria', 'atomic', 'automatic'],
33
- wcagSc: ['1.1.1'],
34
- normativeMappings: [
35
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
36
- ],
37
- defaultSeverity: 'serious',
38
- category: 'perceivable',
39
- type: 'automatic',
40
- defaultConfidence: 'high',
41
- coverage: {
42
- facetsBySc: {
43
- '1.1.1': ['role-img-text-alternative-present']
44
- }
24
+ title: '[role="img"] must have an accessible text alternative',
25
+ description:
26
+ 'Checks that elements with role="img" provide an accessible text alternative via aria-label, aria-labelledby, or a title attribute.',
27
+ i18n: {
28
+ titleKey: 'roleImg_textAlternativePresent_title',
29
+ descriptionKey: 'roleImg_textAlternativePresent_description'
30
+ },
31
+ helpUrl: null,
32
+ tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'aria', 'atomic', 'automatic'],
33
+ wcagSc: ['1.1.1'],
34
+ normativeMappings: [
35
+ {
36
+ standard: 'WCAG',
37
+ version: '2.2',
38
+ requirement: '1.1.1',
39
+ title: 'Non-text Content',
40
+ conformanceLevel: 'A'
41
+ }
42
+ ],
43
+ defaultSeverity: 'serious',
44
+ category: 'perceivable',
45
+ type: 'automatic',
46
+ defaultConfidence: 'high',
47
+ coverage: {
48
+ facetsBySc: {
49
+ '1.1.1': ['role-img-text-alternative-present']
45
50
  }
51
+ }
46
52
  };
47
53
 
48
54
  function runInPage(ctx) {
49
- const { root, helpers, rule } = ctx;
50
- const safeRoot = root || document;
51
-
52
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
- const queryAll = helpers && typeof helpers.queryAll === 'function'
54
- ? helpers.queryAll
55
- : (sel) => {
56
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
57
- catch { return []; }
55
+ const { root, helpers, rule } = ctx;
56
+ const safeRoot = root || document;
57
+
58
+ const queryAllSmart =
59
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
60
+ const queryAll =
61
+ helpers && typeof helpers.queryAll === 'function'
62
+ ? helpers.queryAll
63
+ : (sel) => {
64
+ try {
65
+ return safeRoot && safeRoot.querySelectorAll
66
+ ? Array.from(safeRoot.querySelectorAll(sel))
67
+ : [];
68
+ } catch {
69
+ return [];
70
+ }
58
71
  };
59
72
 
60
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
61
- ? helpers.getEligibilityInfo
62
- : null;
73
+ const getEligibilityInfo =
74
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
63
75
 
64
- const trim = (v) => {
65
- try { return String(v == null ? '' : v).replace(/\s+/g, ' ').trim(); }
66
- catch { return ''; }
67
- };
68
-
69
- const imgElements = (() => {
70
- // do not consider element "img" because it has its own rule
71
- const sel = '[role="img" i]:not(img)';
72
- try { return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []); }
73
- catch { return queryAll(sel); }
74
- })();
75
-
76
- if (!imgElements.length) {
77
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
76
+ const trim = (v) => {
77
+ try {
78
+ return String(v == null ? '' : v)
79
+ .replace(/\s+/g, ' ')
80
+ .trim();
81
+ } catch {
82
+ return '';
83
+ }
84
+ };
85
+
86
+ const imgElements = (() => {
87
+ // do not consider element "img" because it has its own rule
88
+ const sel = '[role="img" i]:not(img)';
89
+ try {
90
+ return Array.from((queryAllSmart ? queryAllSmart(sel) : queryAll(sel)) || []);
91
+ } catch {
92
+ return queryAll(sel);
78
93
  }
94
+ })();
79
95
 
80
- const occurrences = [];
81
- let applicableCount = 0;
96
+ if (!imgElements.length) {
97
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
98
+ }
82
99
 
83
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
84
- ? helpers.isAccTreeEligible
85
- : null;
100
+ const occurrences = [];
101
+ let applicableCount = 0;
86
102
 
87
- const getAccessibleNameInfo = helpers && typeof helpers.getAccessibleNameInfo === 'function'
88
- ? helpers.getAccessibleNameInfo
89
- : null;
103
+ const isAccTreeEligible =
104
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
90
105
 
91
- for (const el of imgElements) {
92
- if (!el || !el.getAttribute) continue;
106
+ const getAccessibleNameInfo =
107
+ helpers && typeof helpers.getAccessibleNameInfo === 'function'
108
+ ? helpers.getAccessibleNameInfo
109
+ : null;
93
110
 
94
- // Applicability: eligible in the acc tree (with helper exceptions).
95
- if (isAccTreeEligible) {
96
- const elig = (() => {
97
- try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; }
98
- })();
99
- if (elig && elig.eligible === false) continue;
100
- }
111
+ for (const el of imgElements) {
112
+ if (!el || !el.getAttribute) continue;
101
113
 
102
- applicableCount += 1;
103
-
104
- // Expectation: aria-label OR aria-labelledby. We use helper name-info when available,
105
- // but we also validate the source to keep this rule scoped/deterministic.
106
-
107
- const ariaLabelRaw = (() => { try { return el.getAttribute('aria-label'); } catch { return null; } })();
108
- const ariaLabel = trim(ariaLabelRaw);
109
-
110
- const ariaLabelledbyRaw = (() => { try { return el.getAttribute('aria-labelledby'); } catch { return null; } })();
111
- const ariaLabelledby = trim(ariaLabelledbyRaw);
112
-
113
- const hasAriaLabelAttr = ariaLabelRaw !== null;
114
- const hasAriaLabelledbyAttr = ariaLabelledbyRaw !== null;
115
-
116
- const hasValidAriaLabel = hasAriaLabelAttr && ariaLabel.length > 0;
117
- const hasValidAriaLabelledbyAttr = hasAriaLabelledbyAttr && ariaLabelledby.length > 0;
118
-
119
- // Last-resort naming mechanism per HTML-AAM (also accepted by
120
- // a widely-used reference engine's role-img-alt): a non-empty title attribute.
121
- const titleRaw = (() => { try { return el.getAttribute('title'); } catch { return null; } })();
122
- const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
123
-
124
- let nameInfo = null;
125
-
126
- // Fast outcomes first (no helper needed)
127
- let reasonCode = '';
128
- let hasName = false;
129
-
130
- if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
131
- if (hasValidTitle) {
132
- hasName = true;
133
- } else {
134
- reasonCode = 'missingTextAlternative';
135
- }
136
- } else if (hasAriaLabelAttr && !hasValidAriaLabel) {
137
- if (hasValidTitle) {
138
- hasName = true;
139
- } else {
140
- reasonCode = 'emptyAriaLabel';
141
- }
142
- } else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
143
- if (hasValidTitle) {
144
- hasName = true;
145
- } else {
146
- reasonCode = 'emptyAriaLabelledby';
147
- }
148
- } else {
149
- // Mechanism present + non-empty. Optionally validate resolution via helper.
150
- if (getAccessibleNameInfo) {
151
- nameInfo = (() => { try { return getAccessibleNameInfo(el, ctx); } catch { return null; } })();
152
- const helperSaysHasName = !!(nameInfo && nameInfo.present && trim(nameInfo.value));
153
- if (helperSaysHasName) {
154
- hasName = true;
155
- } else {
156
- reasonCode = 'nameNotResolved';
157
- }
158
- } else {
159
- // Without helper, accept non-empty aria-label/labelledby as sufficient.
160
- hasName = true;
161
- }
114
+ // Applicability: eligible in the acc tree (with helper exceptions).
115
+ if (isAccTreeEligible) {
116
+ const elig = (() => {
117
+ try {
118
+ return isAccTreeEligible(el, ctx);
119
+ } catch {
120
+ return { eligible: true, reasons: [] };
162
121
  }
122
+ })();
123
+ if (elig && elig.eligible === false) continue;
124
+ }
163
125
 
164
- if (hasName) continue;
165
-
166
-
167
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
168
-
169
- const baseOccurrence = {
170
- summary: 'Missing text alternative on element with role="img".',
171
- hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this image a text alternative.',
172
- i18n: {
173
- summaryKey: 'roleImg_textAlternativePresent_summary_fail',
174
- hintKey: 'roleImg_textAlternativePresent_hint_fail',
175
- params: { role: 'img' }
176
- },
177
- data: {
178
- details: {
179
- reasonCode,
180
- ariaLabel: ariaLabelRaw === null ? null : ariaLabel,
181
- ariaLabelledby: ariaLabelledbyRaw === null ? null : ariaLabelledby,
182
- accessibleNameInfo: nameInfo || null
183
- },
184
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
185
- }
186
- };
126
+ applicableCount += 1;
127
+
128
+ // Expectation: aria-label OR aria-labelledby. We use helper name-info when available,
129
+ // but we also validate the source to keep this rule scoped/deterministic.
187
130
 
188
- if (helpers && typeof helpers.reportOccurrence === 'function') {
189
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
131
+ const ariaLabelRaw = (() => {
132
+ try {
133
+ return el.getAttribute('aria-label');
134
+ } catch {
135
+ return null;
136
+ }
137
+ })();
138
+ const ariaLabel = trim(ariaLabelRaw);
139
+
140
+ const ariaLabelledbyRaw = (() => {
141
+ try {
142
+ return el.getAttribute('aria-labelledby');
143
+ } catch {
144
+ return null;
145
+ }
146
+ })();
147
+ const ariaLabelledby = trim(ariaLabelledbyRaw);
148
+
149
+ const hasAriaLabelAttr = ariaLabelRaw !== null;
150
+ const hasAriaLabelledbyAttr = ariaLabelledbyRaw !== null;
151
+
152
+ const hasValidAriaLabel = hasAriaLabelAttr && ariaLabel.length > 0;
153
+ const hasValidAriaLabelledbyAttr = hasAriaLabelledbyAttr && ariaLabelledby.length > 0;
154
+
155
+ // Last-resort naming mechanism per HTML-AAM (also accepted by
156
+ // a widely-used reference engine's role-img-alt): a non-empty title attribute.
157
+ const titleRaw = (() => {
158
+ try {
159
+ return el.getAttribute('title');
160
+ } catch {
161
+ return null;
162
+ }
163
+ })();
164
+ const hasValidTitle = titleRaw !== null && trim(titleRaw).length > 0;
165
+
166
+ let nameInfo = null;
167
+
168
+ // Fast outcomes first (no helper needed)
169
+ let reasonCode = '';
170
+ let hasName = false;
171
+
172
+ if (!hasAriaLabelAttr && !hasAriaLabelledbyAttr) {
173
+ if (hasValidTitle) {
174
+ hasName = true;
175
+ } else {
176
+ reasonCode = 'missingTextAlternative';
177
+ }
178
+ } else if (hasAriaLabelAttr && !hasValidAriaLabel) {
179
+ if (hasValidTitle) {
180
+ hasName = true;
181
+ } else {
182
+ reasonCode = 'emptyAriaLabel';
183
+ }
184
+ } else if (hasAriaLabelledbyAttr && !hasValidAriaLabelledbyAttr) {
185
+ if (hasValidTitle) {
186
+ hasName = true;
187
+ } else {
188
+ reasonCode = 'emptyAriaLabelledby';
189
+ }
190
+ } else {
191
+ // Mechanism present + non-empty. Optionally validate resolution via helper.
192
+ if (getAccessibleNameInfo) {
193
+ nameInfo = (() => {
194
+ try {
195
+ return getAccessibleNameInfo(el, ctx);
196
+ } catch {
197
+ return null;
198
+ }
199
+ })();
200
+ const helperSaysHasName = !!(nameInfo && nameInfo.present && trim(nameInfo.value));
201
+ if (helperSaysHasName) {
202
+ hasName = true;
190
203
  } else {
191
- occurrences.push({ selector: '', html: '', ...baseOccurrence });
204
+ reasonCode = 'nameNotResolved';
192
205
  }
206
+ } else {
207
+ // Without helper, accept non-empty aria-label/labelledby as sufficient.
208
+ hasName = true;
209
+ }
193
210
  }
194
211
 
195
- if (applicableCount === 0) {
196
- return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
197
- }
212
+ if (hasName) continue;
213
+
214
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
215
+
216
+ const baseOccurrence = {
217
+ summary: 'Missing text alternative on element with role="img".',
218
+ hint: 'Provide aria-label or aria-labelledby (referencing non-empty text) to give this image a text alternative.',
219
+ i18n: {
220
+ summaryKey: 'roleImg_textAlternativePresent_summary_fail',
221
+ hintKey: 'roleImg_textAlternativePresent_hint_fail',
222
+ params: { role: 'img' }
223
+ },
224
+ data: {
225
+ details: {
226
+ reasonCode,
227
+ ariaLabel: ariaLabelRaw === null ? null : ariaLabel,
228
+ ariaLabelledby: ariaLabelledbyRaw === null ? null : ariaLabelledby,
229
+ accessibleNameInfo: nameInfo || null
230
+ },
231
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] }
232
+ }
233
+ };
198
234
 
199
- if (!occurrences.length) {
200
- return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
235
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
236
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
237
+ } else {
238
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
201
239
  }
202
-
203
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
240
+ }
241
+
242
+ if (applicableCount === 0) {
243
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
244
+ }
245
+
246
+ if (!occurrences.length) {
247
+ return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
248
+ }
249
+
250
+ return {
251
+ ruleId: rule.ruleId,
252
+ outcome: 'fail',
253
+ severity: rule.defaultSeverity || 'minor',
254
+ occurrences
255
+ };
204
256
  }
205
257
 
206
258
  module.exports = { id, meta, runInPage };
@@ -10,10 +10,16 @@ const meta = {
10
10
  descriptionKey: 'searchboxNamePresent_description'
11
11
  },
12
12
  helpUrl: null,
13
- tags: ["wcag2a", "wcag412", "forms", "atomic", "automatic", "name", "searchbox"],
13
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'searchbox'],
14
14
  wcagSc: ['4.1.2'],
15
15
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
16
+ {
17
+ standard: 'WCAG',
18
+ version: '2.2',
19
+ requirement: '4.1.2',
20
+ title: 'Name, Role, Value',
21
+ conformanceLevel: 'A'
22
+ }
17
23
  ],
18
24
  defaultSeverity: 'serious',
19
25
  category: 'robust',
@@ -24,20 +30,22 @@ const meta = {
24
30
 
25
31
  function runInPage(ctx) {
26
32
  const { document, helpers, rule } = ctx;
27
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28
- ? helpers.getEligibilityInfo
29
- : null;
30
-
33
+ const getEligibilityInfo =
34
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
31
35
 
32
36
  function normalizeWs(s) {
33
- return String(s || '').replace(/\s+/g, ' ').trim();
37
+ return String(s || '')
38
+ .replace(/\s+/g, ' ')
39
+ .trim();
34
40
  }
35
41
 
36
42
  function getAttr(el, name) {
37
43
  try {
38
44
  if (!el || !el.getAttribute) return '';
39
45
  return normalizeWs(el.getAttribute(name));
40
- } catch { return ''; }
46
+ } catch {
47
+ return '';
48
+ }
41
49
  }
42
50
 
43
51
  function getConservativeSubtreeText(document, container) {
@@ -52,7 +60,7 @@ function runInPage(ctx) {
52
60
  const info = helpers.getContentNameInfo(container, ctx);
53
61
  return info && info.present ? info.value : '';
54
62
  }
55
- const t = (container && container.textContent) ? String(container.textContent) : '';
63
+ const t = container && container.textContent ? String(container.textContent) : '';
56
64
  return t.replace(/\s+/g, ' ').trim();
57
65
  }
58
66
 
@@ -100,7 +108,8 @@ function runInPage(ctx) {
100
108
  }
101
109
 
102
110
  function isEligibleAcc(helpers, el, ctx) {
103
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
111
+ const fn =
112
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
104
113
  if (!fn) return true;
105
114
  try {
106
115
  const r = fn(el, ctx);
@@ -126,12 +135,13 @@ function runInPage(ctx) {
126
135
  return map;
127
136
  }
128
137
 
129
-
130
138
  const occurrences = [];
131
139
  let applicableCount = 0;
132
140
 
133
- const selector = "[role=\"searchbox\"]";
134
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
141
+ const selector = '[role="searchbox"]';
142
+ const nodes = helpers.queryAllSmart
143
+ ? helpers.queryAllSmart(selector)
144
+ : helpers.queryAll(selector);
135
145
 
136
146
  // Precompute label[for] map for searchbox elements that are labelable
137
147
  // native form controls (e.g. <input role="searchbox">).
@@ -200,16 +210,22 @@ function runInPage(ctx) {
200
210
  if (res.ok) continue;
201
211
 
202
212
  const eligInfo = getEligibilityInfo
203
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
204
- : null;
213
+ ? (() => {
214
+ try {
215
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
216
+ } catch {
217
+ return null;
218
+ }
219
+ })()
220
+ : null;
205
221
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
206
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
222
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
207
223
 
208
224
  occurrences.push({
209
225
  selector: stableSelector,
210
226
  html,
211
227
  summary: 'This element has no accessible name.',
212
- hint: 'Provide aria-label or aria-labelledby (preferred), or provide visible text that is not hidden from assistive technologies.',
228
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this searchbox's accessible name.",
213
229
  i18n: {
214
230
  summaryKey: 'searchboxNamePresent_summary_fail',
215
231
  hintKey: 'searchboxNamePresent_hint_fail',
@@ -222,12 +238,16 @@ function runInPage(ctx) {
222
238
  });
223
239
  }
224
240
 
225
-
226
241
  if (applicableCount === 0) {
227
242
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
228
243
  }
229
244
  if (occurrences.length) {
230
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
245
+ return {
246
+ ruleId: rule.ruleId,
247
+ outcome: 'fail',
248
+ severity: rule.defaultSeverity || 'minor',
249
+ occurrences
250
+ };
231
251
  }
232
252
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
233
253
  }