@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
@@ -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="meter"]';
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');
@@ -124,16 +117,22 @@ function runInPage(ctx) {
124
117
  if (res.ok) continue;
125
118
 
126
119
  const eligInfo = getEligibilityInfo
127
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
128
- : null;
120
+ ? (() => {
121
+ try {
122
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
123
+ } catch {
124
+ return null;
125
+ }
126
+ })()
127
+ : null;
129
128
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
130
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
129
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
131
130
 
132
131
  occurrences.push({
133
132
  selector: stableSelector,
134
133
  html,
135
134
  summary: 'This meter has no accessible name.',
136
- hint: 'Provide meter text that is not hidden from assistive technologies, or provide aria-label or aria-labelledby.',
135
+ hint: "Provide aria-label, aria-labelledby, or a title attribute — visible text content is not exposed as this meter's accessible name.",
137
136
  i18n: {
138
137
  summaryKey: 'meterNamePresent_summary_fail',
139
138
  hintKey: 'meterNamePresent_hint_fail',
@@ -146,14 +145,18 @@ function runInPage(ctx) {
146
145
  });
147
146
  }
148
147
 
149
-
150
148
  if (applicableCount === 0) {
151
149
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
152
150
  }
153
151
  if (occurrences.length) {
154
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
152
+ return {
153
+ ruleId: rule.ruleId,
154
+ outcome: 'fail',
155
+ severity: rule.defaultSeverity || 'minor',
156
+ occurrences
157
+ };
155
158
  }
156
159
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
157
160
  }
158
161
 
159
- module.exports = { id, meta, runInPage };
162
+ module.exports = { id, meta, runInPage };
@@ -34,7 +34,8 @@ const id = 'nested-interactive-controls-absent';
34
34
 
35
35
  const meta = {
36
36
  title: 'Interactive controls must not be nested',
37
- description: 'Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.',
37
+ description:
38
+ 'Checks that an interactive control (link, button, form control, or ARIA widget role) does not contain another interactive control.',
38
39
  i18n: {
39
40
  titleKey: 'nestedInteractiveControlsAbsent_title',
40
41
  descriptionKey: 'nestedInteractiveControlsAbsent_description'
@@ -43,7 +44,13 @@ const meta = {
43
44
  tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic'],
44
45
  wcagSc: ['4.1.2'],
45
46
  normativeMappings: [
46
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
47
+ {
48
+ standard: 'WCAG',
49
+ version: '2.2',
50
+ requirement: '4.1.2',
51
+ title: 'Name, Role, Value',
52
+ conformanceLevel: 'A'
53
+ }
47
54
  ],
48
55
  defaultSeverity: 'serious',
49
56
  category: 'robust',
@@ -53,7 +60,7 @@ const meta = {
53
60
  };
54
61
 
55
62
  function runInPage(ctx) {
56
- const { document, helpers, rule } = ctx;
63
+ const { helpers, rule } = ctx;
57
64
 
58
65
  // Declared inside runInPage — see scripts/build-core.js header
59
66
  // ("runInPage MUST be self-contained").
@@ -82,7 +89,9 @@ function runInPage(ctx) {
82
89
  '[role="treeitem"]'
83
90
  ].join(', ');
84
91
 
85
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(INTERACTIVE_SELECTOR) : helpers.queryAll(INTERACTIVE_SELECTOR);
92
+ const nodes = helpers.queryAllSmart
93
+ ? helpers.queryAllSmart(INTERACTIVE_SELECTOR)
94
+ : helpers.queryAll(INTERACTIVE_SELECTOR);
86
95
 
87
96
  const occurrences = [];
88
97
  let applicableCount = 0;
@@ -92,7 +101,7 @@ function runInPage(ctx) {
92
101
 
93
102
  applicableCount += 1;
94
103
 
95
- let nested = [];
104
+ let nested;
96
105
  try {
97
106
  nested = Array.from(el.querySelectorAll(INTERACTIVE_SELECTOR));
98
107
  } catch {
@@ -101,10 +110,13 @@ function runInPage(ctx) {
101
110
  if (!nested.length) continue;
102
111
 
103
112
  const nestedTags = nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown'));
113
+ const dedupedNestedTags = [
114
+ ...new Set(nested.map((n) => (n && n.tagName ? n.tagName.toLowerCase() : 'unknown')))
115
+ ];
104
116
 
105
117
  const tag = el.tagName.toLowerCase();
106
118
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
107
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
119
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
108
120
 
109
121
  occurrences.push({
110
122
  selector: stableSelector,
@@ -114,10 +126,14 @@ function runInPage(ctx) {
114
126
  i18n: {
115
127
  summaryKey: 'nestedInteractiveControlsAbsent_summary_fail',
116
128
  hintKey: 'nestedInteractiveControlsAbsent_hint_fail',
117
- params: { element: tag, nestedElements: nestedTags.join(', ') }
129
+ params: { element: tag, nestedElements: dedupedNestedTags.join(', ') }
118
130
  },
119
131
  data: {
120
- details: { reasonCode: 'NESTED_INTERACTIVE_CONTROL', element: tag, nestedElements: nestedTags }
132
+ details: {
133
+ reasonCode: 'NESTED_INTERACTIVE_CONTROL',
134
+ element: tag,
135
+ nestedElements: nestedTags
136
+ }
121
137
  }
122
138
  });
123
139
  }
@@ -126,9 +142,14 @@ function runInPage(ctx) {
126
142
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
127
143
  }
128
144
  if (occurrences.length) {
129
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
145
+ return {
146
+ ruleId: rule.ruleId,
147
+ outcome: 'fail',
148
+ severity: rule.defaultSeverity || 'serious',
149
+ occurrences
150
+ };
130
151
  }
131
152
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
132
153
  }
133
154
 
134
- module.exports = { id, meta, runInPage };
155
+ module.exports = { id, meta, runInPage };
@@ -22,7 +22,8 @@ const id = 'object-text-alternative-present';
22
22
 
23
23
  const meta = {
24
24
  title: '<object> must provide a text alternative',
25
- description: 'Checks that <object> elements provide a text alternative via fallback content or an accessible name.',
25
+ description:
26
+ 'Checks that <object> elements provide a text alternative via fallback content or an accessible name.',
26
27
  i18n: {
27
28
  titleKey: 'object_textAltPresent_title',
28
29
  descriptionKey: 'object_textAltPresent_description'
@@ -31,7 +32,13 @@ const meta = {
31
32
  tags: ['wcag2a', 'wcag111', 'nontext', 'object', 'atomic', 'automatic'],
32
33
  wcagSc: ['1.1.1'],
33
34
  normativeMappings: [
34
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
35
+ {
36
+ standard: 'WCAG',
37
+ version: '2.2',
38
+ requirement: '1.1.1',
39
+ title: 'Non-text Content',
40
+ conformanceLevel: 'A'
41
+ }
35
42
  ],
36
43
  defaultSeverity: 'serious',
37
44
  category: 'perceivable',
@@ -44,33 +51,39 @@ function runInPage(ctx) {
44
51
  const { document, root, helpers, rule } = ctx;
45
52
  const safeRoot = root || document;
46
53
 
47
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
48
- const queryAll = helpers && typeof helpers.queryAll === 'function'
49
- ? helpers.queryAll
50
- : (sel) => {
51
- try { return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : []; }
52
- catch { return []; }
53
- };
54
-
55
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
56
- ? helpers.getEligibilityInfo
57
- : null;
58
-
59
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
60
- ? helpers.isAccTreeEligible
61
- : null;
62
-
63
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
64
- ? helpers.getFocusableInfo
65
- : null;
66
-
67
- const getAriaNameInfo = helpers && typeof helpers.getAriaNameInfo === 'function'
68
- ? helpers.getAriaNameInfo
69
- : null;
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
+ }
67
+ };
68
+
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;
74
+
75
+ const getFocusableInfo =
76
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
77
+
78
+ const getAriaNameInfo =
79
+ helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
70
80
 
71
81
  const objects = (() => {
72
- try { return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []); }
73
- catch { return queryAll('object'); }
82
+ try {
83
+ return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []);
84
+ } catch {
85
+ return queryAll('object');
86
+ }
74
87
  })();
75
88
 
76
89
  if (!objects.length) {
@@ -88,11 +101,22 @@ function runInPage(ctx) {
88
101
  let aria = null;
89
102
 
90
103
  if (getAriaNameInfo) {
91
- aria = (() => { try { return getAriaNameInfo(el, ctx); } catch { return null; } })();
104
+ aria = (() => {
105
+ try {
106
+ return getAriaNameInfo(el, ctx);
107
+ } catch {
108
+ return null;
109
+ }
110
+ })();
92
111
  }
93
112
 
94
113
  if (aria && aria.present && trim(aria.value)) {
95
- return { present: true, value: trim(aria.value), mechanism: aria.mechanism || 'aria', flags: (aria.flags || []).slice(0) };
114
+ return {
115
+ present: true,
116
+ value: trim(aria.value),
117
+ mechanism: aria.mechanism || 'aria',
118
+ flags: (aria.flags || []).slice(0)
119
+ };
96
120
  }
97
121
 
98
122
  const title = trim(el.getAttribute && el.getAttribute('title'));
@@ -123,20 +147,43 @@ function runInPage(ctx) {
123
147
  if (!el || !el.getAttribute) continue;
124
148
 
125
149
  if (isAccTreeEligible) {
126
- const elig = (() => { try { return isAccTreeEligible(el, ctx); } catch { return { eligible: true, reasons: [] }; } })();
150
+ const elig = (() => {
151
+ try {
152
+ return isAccTreeEligible(el, ctx);
153
+ } catch {
154
+ return { eligible: true, reasons: [] };
155
+ }
156
+ })();
127
157
  if (elig && elig.eligible === false) continue;
128
158
  }
129
159
 
130
160
  // role presentation/none exclusion only when not focusable
131
- const role = (() => { try { return String(el.getAttribute('role') || '').trim().toLowerCase(); } catch { return ''; } })();
161
+ const role = (() => {
162
+ try {
163
+ return String(el.getAttribute('role') || '')
164
+ .trim()
165
+ .toLowerCase();
166
+ } catch {
167
+ return '';
168
+ }
169
+ })();
132
170
  if (role === 'presentation' || role === 'none') {
133
- let focusable = false;
171
+ let focusable;
134
172
  if (getFocusableInfo) {
135
- const fi = (() => { try { return getFocusableInfo(el, ctx); } catch { return null; } })();
173
+ const fi = (() => {
174
+ try {
175
+ return getFocusableInfo(el, ctx);
176
+ } catch {
177
+ return null;
178
+ }
179
+ })();
136
180
  focusable = !!(fi && fi.focusable);
137
181
  } else {
138
182
  const tabindex = el.getAttribute('tabindex');
139
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
183
+ focusable =
184
+ tabindex != null &&
185
+ String(tabindex).trim() !== '' &&
186
+ !Number.isNaN(Number(String(tabindex).trim()));
140
187
  }
141
188
  if (!focusable) continue;
142
189
  }
@@ -147,18 +194,18 @@ function runInPage(ctx) {
147
194
 
148
195
  // Only compute fallback text if there is no name.
149
196
  // (textContent can be expensive; avoid when not needed)
150
- const fb = name.present ? { present: false, value: '', mechanism: 'fallback', flags: ['skipped-name-present'] }
151
- : computeFallbackText(el);
197
+ const fb = name.present
198
+ ? { present: false, value: '', mechanism: 'fallback', flags: ['skipped-name-present'] }
199
+ : computeFallbackText(el);
152
200
 
153
201
  const hasTextAlt = !!(name.present || fb.present);
154
202
  if (hasTextAlt) continue;
155
203
 
156
-
157
204
  const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
158
205
 
159
206
  const baseOccurrence = {
160
207
  summary: 'Missing text alternative for <object>.',
161
- hint: 'Provide meaningful fallback content inside <object>, or add an accessible name (aria-label/aria-labelledby).',
208
+ hint: 'Provide meaningful fallback content inside <object>, add an accessible name (aria-label/aria-labelledby), or use a title attribute as a best-effort fallback.',
162
209
  i18n: {
163
210
  summaryKey: 'object_textAltPresent_summary_fail',
164
211
  hintKey: 'object_textAltPresent_hint_fail',
@@ -187,7 +234,12 @@ function runInPage(ctx) {
187
234
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
188
235
  }
189
236
 
190
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
237
+ return {
238
+ ruleId: rule.ruleId,
239
+ outcome: 'fail',
240
+ severity: rule.defaultSeverity || 'minor',
241
+ occurrences
242
+ };
191
243
  }
192
244
 
193
245
  module.exports = { id, meta, runInPage };
@@ -10,10 +10,16 @@ const meta = {
10
10
  descriptionKey: 'optionNamePresent_description'
11
11
  },
12
12
  helpUrl: null,
13
- tags: ["wcag2a", "wcag412", "forms", "atomic", "automatic", "name", "option"],
13
+ tags: ['wcag2a', 'wcag412', 'forms', 'atomic', 'automatic', 'name', 'option'],
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
 
@@ -74,7 +82,8 @@ function runInPage(ctx) {
74
82
  }
75
83
 
76
84
  function isEligibleAcc(helpers, el, ctx) {
77
- const fn = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
85
+ const fn =
86
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
78
87
  if (!fn) return true;
79
88
  try {
80
89
  const r = fn(el, ctx);
@@ -85,13 +94,13 @@ function runInPage(ctx) {
85
94
  }
86
95
  }
87
96
 
88
-
89
97
  const occurrences = [];
90
98
  let applicableCount = 0;
91
99
 
92
- const selector = "[role=\"option\"]";
93
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
94
-
100
+ const selector = '[role="option"]';
101
+ const nodes = helpers.queryAllSmart
102
+ ? helpers.queryAllSmart(selector)
103
+ : helpers.queryAll(selector);
95
104
 
96
105
  function hasName(el) {
97
106
  const ariaLabel = getAttr(el, 'aria-label');
@@ -103,11 +112,9 @@ function runInPage(ctx) {
103
112
  const title = getAttr(el, 'title');
104
113
  if (title) return { ok: true, method: 'title' };
105
114
 
106
-
107
115
  const t = getConservativeSubtreeText(document, el);
108
116
  if (t) return { ok: true, method: 'content' };
109
117
 
110
-
111
118
  return { ok: false, method: 'none' };
112
119
  }
113
120
 
@@ -121,10 +128,16 @@ function runInPage(ctx) {
121
128
  if (res.ok) continue;
122
129
 
123
130
  const eligInfo = getEligibilityInfo
124
- ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
125
- : null;
131
+ ? (() => {
132
+ try {
133
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
134
+ } catch {
135
+ return null;
136
+ }
137
+ })()
138
+ : null;
126
139
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
127
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
140
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
128
141
 
129
142
  occurrences.push({
130
143
  selector: stableSelector,
@@ -143,12 +156,16 @@ function runInPage(ctx) {
143
156
  });
144
157
  }
145
158
 
146
-
147
159
  if (applicableCount === 0) {
148
160
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
149
161
  }
150
162
  if (occurrences.length) {
151
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
163
+ return {
164
+ ruleId: rule.ruleId,
165
+ outcome: 'fail',
166
+ severity: rule.defaultSeverity || 'minor',
167
+ occurrences
168
+ };
152
169
  }
153
170
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
154
171
  }
@@ -13,7 +13,13 @@ const meta = {
13
13
  tags: ['wcag2a', 'wcag242', 'titles', 'atomic', 'navigation', 'automatic'],
14
14
  wcagSc: ['2.4.2'],
15
15
  normativeMappings: [
16
- { standard: 'WCAG', version: '2.2', requirement: '2.4.2', title: 'Page Titled', conformanceLevel: 'A' }
16
+ {
17
+ standard: 'WCAG',
18
+ version: '2.2',
19
+ requirement: '2.4.2',
20
+ title: 'Page Titled',
21
+ conformanceLevel: 'A'
22
+ }
17
23
  ],
18
24
  defaultSeverity: 'serious',
19
25
  category: 'operable',
@@ -54,13 +60,13 @@ function runInPage(ctx) {
54
60
  selector: 'head > title',
55
61
  html: '',
56
62
  summary: missingTitleEl
57
- ? 'The page is missing a <title> element.'
58
- : 'The page has an empty <title>.',
63
+ ? 'The page is missing a <title> element.'
64
+ : 'The page has an empty <title>.',
59
65
  hint: 'Provide a descriptive, non-empty <title> that identifies the page topic or purpose.',
60
66
  i18n: {
61
67
  summaryKey: missingTitleEl
62
- ? 'pageTitlePresent_summary_fail_missing'
63
- : 'pageTitlePresent_summary_fail_empty',
68
+ ? 'pageTitlePresent_summary_fail_missing'
69
+ : 'pageTitlePresent_summary_fail_empty',
64
70
  hintKey: 'pageTitlePresent_hint_fail',
65
71
  params: {}
66
72
  },
@@ -86,7 +92,12 @@ function runInPage(ctx) {
86
92
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
87
93
  }
88
94
  if (occurrences.length) {
89
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
95
+ return {
96
+ ruleId: rule.ruleId,
97
+ outcome: 'fail',
98
+ severity: rule.defaultSeverity || 'minor',
99
+ occurrences
100
+ };
90
101
  }
91
102
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
92
103
  }