@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
@@ -27,7 +27,8 @@ const id = 'html-xml-lang-mismatch';
27
27
 
28
28
  const meta = {
29
29
  title: 'lang and xml:lang must not disagree',
30
- description: 'Checks that the <html> element\'s lang and xml:lang attributes declare the same primary language, when both are present.',
30
+ description:
31
+ "Checks that the <html> element's lang and xml:lang attributes declare the same primary language, when both are present.",
31
32
  i18n: {
32
33
  titleKey: 'htmlXmlLangMismatch_title',
33
34
  descriptionKey: 'htmlXmlLangMismatch_description'
@@ -36,7 +37,13 @@ const meta = {
36
37
  tags: ['wcag2a', 'wcag311', 'structure', 'language', 'atomic', 'automatic'],
37
38
  wcagSc: ['3.1.1'],
38
39
  normativeMappings: [
39
- { standard: 'WCAG', version: '2.2', requirement: '3.1.1', title: 'Language of Page', conformanceLevel: 'A' }
40
+ {
41
+ standard: 'WCAG',
42
+ version: '2.2',
43
+ requirement: '3.1.1',
44
+ title: 'Language of Page',
45
+ conformanceLevel: 'A'
46
+ }
40
47
  ],
41
48
  defaultSeverity: 'serious',
42
49
  category: 'understandable',
@@ -45,11 +52,19 @@ const meta = {
45
52
  coverage: { facetsBySc: { '3.1.1': ['html-xml-lang-mismatch'] } }
46
53
  };
47
54
 
55
+ // This check is inherently whole-document (does the PAGE have this
56
+ // property?), not evaluable per-subtree -- notApplicable when contextSelector
57
+ // scoped this run narrower than the whole document, or when
58
+ // engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
59
+ function applicability(ctx) {
60
+ return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
61
+ }
62
+
48
63
  function runInPage(ctx) {
49
64
  const { document, helpers, rule } = ctx;
50
65
 
51
66
  const html = document && document.documentElement;
52
- const tag = (html && html.tagName) ? String(html.tagName).toLowerCase() : '';
67
+ const tag = html && html.tagName ? String(html.tagName).toLowerCase() : '';
53
68
  if (!html || tag !== 'html') {
54
69
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
55
70
  }
@@ -68,24 +83,33 @@ function runInPage(ctx) {
68
83
  }
69
84
 
70
85
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(html) : 'html';
71
- const htmlSnippet = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(html) : (html.outerHTML || '').slice(0, 200);
86
+ const htmlSnippet = helpers.getOuterHtmlSnippet
87
+ ? helpers.getOuterHtmlSnippet(html)
88
+ : (html.outerHTML || '').slice(0, 200);
72
89
 
73
- const occurrences = [{
74
- selector: stableSelector,
75
- html: htmlSnippet,
76
- summary: `The lang ("${lang}") and xml:lang ("${xmlLang}") attributes declare different languages.`,
77
- hint: 'Make lang and xml:lang declare the same primary language, or remove the deprecated xml:lang attribute.',
78
- i18n: {
79
- summaryKey: 'htmlXmlLangMismatch_summary_fail',
80
- hintKey: 'htmlXmlLangMismatch_hint_fail',
81
- params: { lang, xmlLang }
82
- },
83
- data: {
84
- details: { reasonCode: 'HTML_XML_LANG_MISMATCH', lang, xmlLang }
90
+ const occurrences = [
91
+ {
92
+ selector: stableSelector,
93
+ html: htmlSnippet,
94
+ summary: `The lang ("${lang}") and xml:lang ("${xmlLang}") attributes declare different languages.`,
95
+ hint: 'Make lang and xml:lang declare the same primary language, or remove the deprecated xml:lang attribute.',
96
+ i18n: {
97
+ summaryKey: 'htmlXmlLangMismatch_summary_fail',
98
+ hintKey: 'htmlXmlLangMismatch_hint_fail',
99
+ params: { lang, xmlLang }
100
+ },
101
+ data: {
102
+ details: { reasonCode: 'HTML_XML_LANG_MISMATCH', lang, xmlLang }
103
+ }
85
104
  }
86
- }];
105
+ ];
87
106
 
88
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
107
+ return {
108
+ ruleId: rule.ruleId,
109
+ outcome: 'fail',
110
+ severity: rule.defaultSeverity || 'serious',
111
+ occurrences
112
+ };
89
113
  }
90
114
 
91
- module.exports = { id, meta, runInPage };
115
+ module.exports = { id, meta, runInPage, applicability };
@@ -36,7 +36,8 @@ const id = 'iframe-focusable-content';
36
36
 
37
37
  const meta = {
38
38
  title: 'Frames with tabindex="-1" must not contain focusable content',
39
- description: 'Checks that same-origin <iframe>/<frame> elements with tabindex="-1" do not contain focusable content, since browsers do not propagate that restriction into the frame’s embedded document.',
39
+ description:
40
+ 'Checks that same-origin <iframe>/<frame> elements with tabindex="-1" do not contain focusable content, since browsers do not propagate that restriction into the frame’s embedded document.',
40
41
  i18n: {
41
42
  titleKey: 'iframeFocusableContent_title',
42
43
  descriptionKey: 'iframeFocusableContent_description'
@@ -45,7 +46,13 @@ const meta = {
45
46
  tags: ['wcag2a', 'wcag211', 'structure', 'atomic', 'automatic', 'keyboard', 'iframe'],
46
47
  wcagSc: ['2.1.1'],
47
48
  normativeMappings: [
48
- { standard: 'WCAG', version: '2.2', requirement: '2.1.1', title: 'Keyboard', conformanceLevel: 'A' }
49
+ {
50
+ standard: 'WCAG',
51
+ version: '2.2',
52
+ requirement: '2.1.1',
53
+ title: 'Keyboard',
54
+ conformanceLevel: 'A'
55
+ }
49
56
  ],
50
57
  defaultSeverity: 'moderate',
51
58
  category: 'operable',
@@ -55,15 +62,15 @@ const meta = {
55
62
  };
56
63
 
57
64
  function runInPage(ctx) {
58
- const { document, helpers, rule } = ctx;
65
+ const { helpers, rule } = ctx;
59
66
 
60
67
  function hasFocusableCandidate(doc) {
61
68
  if (!doc || !doc.querySelectorAll) return false;
62
- let els = [];
69
+ let els;
63
70
  try {
64
71
  els = doc.querySelectorAll(
65
72
  'a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), ' +
66
- 'select:not([disabled]), textarea:not([disabled]), iframe, [contenteditable="true"], [tabindex]'
73
+ 'select:not([disabled]), textarea:not([disabled]), iframe, [contenteditable="true"], [tabindex]'
67
74
  );
68
75
  } catch {
69
76
  return false;
@@ -87,7 +94,9 @@ function runInPage(ctx) {
87
94
  return !Number.isNaN(n) && n < 0;
88
95
  }
89
96
 
90
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
97
+ const nodes = helpers.queryAllSmart
98
+ ? helpers.queryAllSmart('iframe, frame')
99
+ : helpers.queryAll('iframe, frame');
91
100
 
92
101
  const occurrences = [];
93
102
  let applicableCount = 0;
@@ -96,7 +105,7 @@ function runInPage(ctx) {
96
105
  if (!el || !el.getAttribute) continue;
97
106
  if (!getNegativeTabIndex(el)) continue;
98
107
 
99
- let contentDoc = null;
108
+ let contentDoc;
100
109
  try {
101
110
  contentDoc = el.contentDocument || null;
102
111
  } catch {
@@ -110,12 +119,13 @@ function runInPage(ctx) {
110
119
 
111
120
  const tag = el.tagName.toLowerCase();
112
121
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
113
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
122
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
114
123
 
115
124
  occurrences.push({
116
125
  selector: stableSelector,
117
126
  html,
118
- summary: 'This frame has tabindex="-1" but its content contains focusable elements, which remain reachable by keyboard.',
127
+ summary:
128
+ 'This frame has tabindex="-1" but its content contains focusable elements, which remain reachable by keyboard.',
119
129
  hint: 'Remove focusable content from the frame, or remove tabindex="-1" if the frame is meant to be reachable.',
120
130
  i18n: {
121
131
  summaryKey: 'iframeFocusableContent_summary_fail',
@@ -132,9 +142,14 @@ function runInPage(ctx) {
132
142
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
133
143
  }
134
144
  if (occurrences.length) {
135
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
145
+ return {
146
+ ruleId: rule.ruleId,
147
+ outcome: 'fail',
148
+ severity: rule.defaultSeverity || 'moderate',
149
+ occurrences
150
+ };
136
151
  }
137
152
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
138
153
  }
139
154
 
140
- module.exports = { id, meta, runInPage };
155
+ module.exports = { id, meta, runInPage };
@@ -25,7 +25,8 @@ const id = 'iframe-name-present';
25
25
 
26
26
  const meta = {
27
27
  title: 'Frames have an accessible name',
28
- description: 'Checks that <iframe>/<frame> elements expose a non-empty accessible name via aria-label, aria-labelledby, or the title attribute.',
28
+ description:
29
+ 'Checks that <iframe>/<frame> elements expose a non-empty accessible name via aria-label, aria-labelledby, or the title attribute.',
29
30
  i18n: {
30
31
  titleKey: 'iframeNamePresent_title',
31
32
  descriptionKey: 'iframeNamePresent_description'
@@ -34,7 +35,13 @@ const meta = {
34
35
  tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'iframe'],
35
36
  wcagSc: ['4.1.2'],
36
37
  normativeMappings: [
37
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
38
+ {
39
+ standard: 'WCAG',
40
+ version: '2.2',
41
+ requirement: '4.1.2',
42
+ title: 'Name, Role, Value',
43
+ conformanceLevel: 'A'
44
+ }
38
45
  ],
39
46
  defaultSeverity: 'serious',
40
47
  category: 'robust',
@@ -44,9 +51,11 @@ const meta = {
44
51
  };
45
52
 
46
53
  function runInPage(ctx) {
47
- const { document, helpers, rule } = ctx;
54
+ const { helpers, rule } = ctx;
48
55
 
49
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
56
+ const nodes = helpers.queryAllSmart
57
+ ? helpers.queryAllSmart('iframe, frame')
58
+ : helpers.queryAll('iframe, frame');
50
59
 
51
60
  const occurrences = [];
52
61
  let applicableCount = 0;
@@ -62,15 +71,23 @@ function runInPage(ctx) {
62
71
 
63
72
  applicableCount += 1;
64
73
 
65
- const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx, { maxRefs: 8 }) : null;
74
+ const nameInfo = helpers.getAccessibleNameInfo
75
+ ? helpers.getAccessibleNameInfo(el, ctx, { maxRefs: 8 })
76
+ : null;
66
77
  if (nameInfo && nameInfo.present && nameInfo.value) continue;
67
78
 
68
79
  const eligInfo = helpers.getEligibilityInfo
69
- ? (() => { try { return helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })()
80
+ ? (() => {
81
+ try {
82
+ return helpers.getEligibilityInfo(el, ctx, { targetSet: 'acc' });
83
+ } catch {
84
+ return null;
85
+ }
86
+ })()
70
87
  : null;
71
88
  const tag = el.tagName.toLowerCase();
72
89
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
73
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
90
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
74
91
 
75
92
  occurrences.push({
76
93
  selector: stableSelector,
@@ -93,9 +110,14 @@ function runInPage(ctx) {
93
110
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
94
111
  }
95
112
  if (occurrences.length) {
96
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
113
+ return {
114
+ ruleId: rule.ruleId,
115
+ outcome: 'fail',
116
+ severity: rule.defaultSeverity || 'serious',
117
+ occurrences
118
+ };
97
119
  }
98
120
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
99
121
  }
100
122
 
101
- module.exports = { id, meta, runInPage };
123
+ module.exports = { id, meta, runInPage };
@@ -28,7 +28,8 @@ const id = 'iframe-title-unique';
28
28
 
29
29
  const meta = {
30
30
  title: 'Frame titles must be unique',
31
- description: 'Checks that no two <iframe>/<frame> elements in scope share the same title attribute value.',
31
+ description:
32
+ 'Checks that no two <iframe>/<frame> elements in scope share the same title attribute value.',
32
33
  i18n: {
33
34
  titleKey: 'iframeTitleUnique_title',
34
35
  descriptionKey: 'iframeTitleUnique_description'
@@ -37,7 +38,13 @@ const meta = {
37
38
  tags: ['wcag2a', 'wcag412', 'structure', 'atomic', 'automatic', 'name', 'iframe'],
38
39
  wcagSc: ['4.1.2'],
39
40
  normativeMappings: [
40
- { standard: 'WCAG', version: '2.2', requirement: '4.1.2', title: 'Name, Role, Value', conformanceLevel: 'A' }
41
+ {
42
+ standard: 'WCAG',
43
+ version: '2.2',
44
+ requirement: '4.1.2',
45
+ title: 'Name, Role, Value',
46
+ conformanceLevel: 'A'
47
+ }
41
48
  ],
42
49
  defaultSeverity: 'moderate',
43
50
  category: 'robust',
@@ -47,9 +54,11 @@ const meta = {
47
54
  };
48
55
 
49
56
  function runInPage(ctx) {
50
- const { document, helpers, rule } = ctx;
57
+ const { helpers, rule } = ctx;
51
58
 
52
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
59
+ const nodes = helpers.queryAllSmart
60
+ ? helpers.queryAllSmart('iframe, frame')
61
+ : helpers.queryAll('iframe, frame');
53
62
 
54
63
  const groups = new Map(); // trimmed title -> elements[]
55
64
  let applicableCount = 0;
@@ -79,7 +88,9 @@ function runInPage(ctx) {
79
88
  for (const el of els) {
80
89
  const tag = el.tagName.toLowerCase();
81
90
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
82
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
91
+ const html = helpers.getOuterHtmlSnippet
92
+ ? helpers.getOuterHtmlSnippet(el)
93
+ : el.outerHTML || '';
83
94
 
84
95
  occurrences.push({
85
96
  selector: stableSelector,
@@ -92,16 +103,26 @@ function runInPage(ctx) {
92
103
  params: { element: tag, title }
93
104
  },
94
105
  data: {
95
- details: { reasonCode: 'IFRAME_TITLE_DUPLICATE', element: tag, title, duplicateCount: els.length }
106
+ details: {
107
+ reasonCode: 'IFRAME_TITLE_DUPLICATE',
108
+ element: tag,
109
+ title,
110
+ duplicateCount: els.length
111
+ }
96
112
  }
97
113
  });
98
114
  }
99
115
  }
100
116
 
101
117
  if (occurrences.length) {
102
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'moderate', occurrences };
118
+ return {
119
+ ruleId: rule.ruleId,
120
+ outcome: 'fail',
121
+ severity: rule.defaultSeverity || 'moderate',
122
+ occurrences
123
+ };
103
124
  }
104
125
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
105
126
  }
106
127
 
107
- module.exports = { id, meta, runInPage };
128
+ module.exports = { id, meta, runInPage };
@@ -20,7 +20,8 @@ const id = 'img-alt-present';
20
20
 
21
21
  const meta = {
22
22
  title: '<img> must have an alt attribute',
23
- description: 'Checks that <img> elements provide an alt attribute to support a text alternative mechanism.',
23
+ description:
24
+ 'Checks that <img> elements provide an alt attribute to support a text alternative mechanism.',
24
25
  i18n: {
25
26
  titleKey: 'img_altPresent_title',
26
27
  descriptionKey: 'img_altPresent_description'
@@ -29,7 +30,13 @@ const meta = {
29
30
  tags: ['wcag2a', 'wcag111', 'nontext', 'images', 'atomic', 'automatic'],
30
31
  wcagSc: ['1.1.1'],
31
32
  normativeMappings: [
32
- { standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A' }
33
+ {
34
+ standard: 'WCAG',
35
+ version: '2.2',
36
+ requirement: '1.1.1',
37
+ title: 'Non-text Content',
38
+ conformanceLevel: 'A'
39
+ }
33
40
  ],
34
41
  defaultSeverity: 'serious',
35
42
  category: 'perceivable',
@@ -46,12 +53,17 @@ function runInPage(ctx) {
46
53
  const { document, root, helpers, rule } = ctx;
47
54
  const safeRoot = root || document;
48
55
 
49
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
50
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
56
+ const queryAllSmart =
57
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
58
+ const getEligibilityInfo =
59
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
51
60
 
52
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
53
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
54
- const getAriaNameInfo = helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
61
+ const isAccTreeEligible =
62
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
63
+ const getFocusableInfo =
64
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
65
+ const getAriaNameInfo =
66
+ helpers && typeof helpers.getAriaNameInfo === 'function' ? helpers.getAriaNameInfo : null;
55
67
 
56
68
  // Prefer tagName collection when available (cheap), otherwise fall back.
57
69
  function getImgsCollection() {
@@ -73,7 +85,8 @@ function runInPage(ctx) {
73
85
  }
74
86
 
75
87
  try {
76
- if (safeRoot && typeof safeRoot.querySelectorAll === 'function') return safeRoot.querySelectorAll('img');
88
+ if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
89
+ return safeRoot.querySelectorAll('img');
77
90
  } catch {
78
91
  // fall through
79
92
  }
@@ -94,19 +107,6 @@ function runInPage(ctx) {
94
107
  // Helper-safe trim
95
108
  const trim = (v) => (v == null ? '' : String(v)).trim();
96
109
 
97
- // Deterministic tabindex fallback (only used if helper missing)
98
- function isFocusableFallback(el) {
99
- try {
100
- const tabindex = el.getAttribute('tabindex');
101
- if (tabindex == null) return false;
102
- const s = String(tabindex).trim();
103
- if (!s) return false;
104
- return !Number.isNaN(Number(s));
105
- } catch {
106
- return false;
107
- }
108
- }
109
-
110
110
  for (let i = 0; i < imgLen; i++) {
111
111
  const el = imgs[i];
112
112
  if (!el || !el.getAttribute) continue;
@@ -125,7 +125,7 @@ function runInPage(ctx) {
125
125
  }
126
126
 
127
127
  // Role (presentation/none) exclusion only when NOT focusable.
128
- let role = '';
128
+ let role;
129
129
  try {
130
130
  role = trim(el.getAttribute('role')).toLowerCase();
131
131
  } catch {
@@ -133,17 +133,13 @@ function runInPage(ctx) {
133
133
  }
134
134
 
135
135
  if (role === 'presentation' || role === 'none') {
136
- let focusable = false;
137
- if (getFocusableInfo) {
138
- try {
139
- const fi = getFocusableInfo(el, ctx);
140
- // Preserve existing behavior: prefer fi.focusable; fall back to fi.tabbable if present.
141
- focusable = !!(fi && (fi.focusable || fi.tabbable));
142
- } catch {
143
- focusable = false;
144
- }
145
- } else {
146
- focusable = isFocusableFallback(el);
136
+ let focusable;
137
+ try {
138
+ const fi = getFocusableInfo(el, ctx);
139
+ // Preserve existing behavior: prefer fi.focusable; fall back to fi.tabbable if present.
140
+ focusable = !!(fi && (fi.focusable || fi.tabbable));
141
+ } catch {
142
+ focusable = false;
147
143
  }
148
144
 
149
145
  if (!focusable) continue;
@@ -153,7 +149,7 @@ function runInPage(ctx) {
153
149
  applicableCount += 1;
154
150
 
155
151
  // alt attribute presence check (empty allowed)
156
- let hasAlt = false;
152
+ let hasAlt;
157
153
  try {
158
154
  hasAlt = el.getAttribute('alt') !== null;
159
155
  } catch {
@@ -167,7 +163,7 @@ function runInPage(ctx) {
167
163
  // image with a non-empty ARIA name is not missing a text alternative
168
164
  // just because it lacks an alt attribute.
169
165
  if (getAriaNameInfo) {
170
- let ariaName = null;
166
+ let ariaName;
171
167
  try {
172
168
  ariaName = getAriaNameInfo(el, ctx);
173
169
  } catch {
@@ -188,7 +184,15 @@ function runInPage(ctx) {
188
184
  const title = trim(el.getAttribute('title'));
189
185
  if (title) continue;
190
186
 
191
- const eligInfo = getEligibilityInfo ? (() => { try { return getEligibilityInfo(el, ctx, { targetSet: 'acc' }); } catch { return null; } })() : null;
187
+ const eligInfo = getEligibilityInfo
188
+ ? (() => {
189
+ try {
190
+ return getEligibilityInfo(el, ctx, { targetSet: 'acc' });
191
+ } catch {
192
+ return null;
193
+ }
194
+ })()
195
+ : null;
192
196
 
193
197
  const baseOccurrence = {
194
198
  summary: 'Missing alt attribute on <img>.',
@@ -201,12 +205,7 @@ function runInPage(ctx) {
201
205
  data: { visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] } }
202
206
  };
203
207
 
204
- if (helpers && typeof helpers.reportOccurrence === 'function') {
205
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
206
- } else {
207
- // Never compute selector/snippet in the rule.
208
- occurrences.push({ __node: el, selector: '', html: '', ...baseOccurrence });
209
- }
208
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
210
209
  }
211
210
 
212
211
  if (applicableCount === 0) {
@@ -217,7 +216,12 @@ function runInPage(ctx) {
217
216
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
218
217
  }
219
218
 
220
- return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
219
+ return {
220
+ ruleId: rule.ruleId,
221
+ outcome: 'fail',
222
+ severity: rule.defaultSeverity || 'minor',
223
+ occurrences
224
+ };
221
225
  }
222
226
 
223
227
  module.exports = { id, meta, runInPage };