@surea11y/core 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +37 -7
  2. package/LICENSE +373 -21
  3. package/README.md +67 -1
  4. package/bin/core.js +144 -19
  5. package/docs/API_STABILITY.md +1 -1
  6. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  7. package/docs/CI_INTEGRATIONS.md +103 -0
  8. package/docs/CLI.md +54 -1
  9. package/docs/ENGINE_OPTIONS.md +2 -0
  10. package/docs/INTEGRATION.md +18 -0
  11. package/docs/OUTPUT_SCHEMA.md +1 -1
  12. package/docs/RULE_CATALOG.md +1 -1
  13. package/docs/SARIF.md +59 -0
  14. package/package.json +15 -4
  15. package/src/baseline.js +0 -0
  16. package/src/catalogs/composites.wcag.js +414 -450
  17. package/src/checks/automatic/area-alt-present.js +59 -25
  18. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  19. package/src/checks/automatic/aria-allowed-role.js +21 -7
  20. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  21. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  22. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  23. package/src/checks/automatic/aria-hidden-body.js +42 -19
  24. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  25. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  26. package/src/checks/automatic/aria-prohibited-children.js +55 -16
  27. package/src/checks/automatic/aria-required-attr.js +23 -8
  28. package/src/checks/automatic/aria-required-children.js +37 -14
  29. package/src/checks/automatic/aria-required-parent.js +48 -14
  30. package/src/checks/automatic/aria-role-name-present.js +47 -21
  31. package/src/checks/automatic/aria-roles-valid.js +22 -12
  32. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  33. package/src/checks/automatic/aria-valid-attr.js +17 -5
  34. package/src/checks/automatic/autocomplete-valid.js +74 -16
  35. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  36. package/src/checks/automatic/binary-control-name-present.js +60 -50
  37. package/src/checks/automatic/button-name-present.js +48 -18
  38. package/src/checks/automatic/bypass-blocks-present.js +42 -25
  39. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  40. package/src/checks/automatic/combobox-name-present.js +38 -45
  41. package/src/checks/automatic/contrast-computable.js +361 -341
  42. package/src/checks/automatic/contrast-enhanced.js +487 -466
  43. package/src/checks/automatic/contrast-minimum.js +486 -465
  44. package/src/checks/automatic/css-orientation-lock.js +30 -8
  45. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  46. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  47. package/src/checks/automatic/dialog-name-present.js +37 -75
  48. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  49. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  50. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  51. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  52. package/src/checks/automatic/form-control-single-label.js +47 -10
  53. package/src/checks/automatic/html-xml-lang-mismatch.js +34 -18
  54. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  55. package/src/checks/automatic/iframe-name-present.js +31 -9
  56. package/src/checks/automatic/iframe-title-unique.js +29 -8
  57. package/src/checks/automatic/img-alt-present.js +47 -43
  58. package/src/checks/automatic/input-image-alt-present.js +143 -112
  59. package/src/checks/automatic/label-in-name.js +50 -22
  60. package/src/checks/automatic/language-page-present.js +109 -109
  61. package/src/checks/automatic/link-in-text-block.js +59 -19
  62. package/src/checks/automatic/link-name-present.js +45 -14
  63. package/src/checks/automatic/list-children-valid.js +26 -9
  64. package/src/checks/automatic/listbox-name-present.js +39 -19
  65. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  66. package/src/checks/automatic/menuitem-name-present.js +39 -61
  67. package/src/checks/automatic/meta-refresh-no-exceptions.js +28 -7
  68. package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
  69. package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -7
  70. package/src/checks/automatic/meter-name-present.js +36 -33
  71. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  72. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  73. package/src/checks/automatic/option-name-present.js +38 -21
  74. package/src/checks/automatic/page-title-present.js +17 -6
  75. package/src/checks/automatic/progressbar-name-present.js +41 -34
  76. package/src/checks/automatic/role-img-alt-present.js +209 -157
  77. package/src/checks/automatic/searchbox-name-present.js +39 -19
  78. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  79. package/src/checks/automatic/slider-name-present.js +40 -47
  80. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  81. package/src/checks/automatic/summary-name-present.js +37 -17
  82. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  83. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  84. package/src/checks/automatic/tab-name-present.js +37 -60
  85. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  86. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  87. package/src/checks/automatic/target-size-minimum.js +118 -48
  88. package/src/checks/automatic/td-has-header.js +29 -11
  89. package/src/checks/automatic/textbox-name-present.js +39 -19
  90. package/src/checks/automatic/tooltip-name-present.js +37 -18
  91. package/src/checks/automatic/treeitem-name-present.js +38 -21
  92. package/src/checks/automatic/valid-lang.js +20 -6
  93. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  94. package/src/checks/manual/accesskeys-manual.js +14 -5
  95. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  96. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  97. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  98. package/src/checks/manual/aria-text-manual.js +14 -6
  99. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  100. package/src/checks/manual/css-hidden-focus.js +196 -165
  101. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  102. package/src/checks/manual/empty-heading-manual.js +24 -7
  103. package/src/checks/manual/empty-table-header-manual.js +17 -6
  104. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  105. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  106. package/src/checks/manual/heading-order-manual.js +22 -7
  107. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  108. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  109. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  110. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  111. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  112. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  113. package/src/checks/manual/label-title-only-manual.js +14 -5
  114. package/src/checks/manual/landmark-banner-is-top-level-manual.js +66 -16
  115. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
  116. package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
  117. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
  118. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
  119. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  120. package/src/checks/manual/landmark-one-main-manual.js +26 -20
  121. package/src/checks/manual/landmark-unique-manual.js +41 -15
  122. package/src/checks/manual/link-name-quality-manual.js +43 -12
  123. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  124. package/src/checks/manual/meta-viewport-large-manual.js +16 -5
  125. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  126. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  127. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  128. package/src/checks/manual/p-as-heading-manual.js +22 -7
  129. package/src/checks/manual/page-has-heading-one-manual.js +31 -22
  130. package/src/checks/manual/page-title-patterns-manual.js +78 -50
  131. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  132. package/src/checks/manual/region-manual.js +241 -48
  133. package/src/checks/manual/scope-attr-valid-manual.js +10 -3
  134. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  135. package/src/checks/manual/skip-link-manual.js +35 -12
  136. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  137. package/src/checks/manual/tabindex-manual.js +10 -3
  138. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  139. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  140. package/src/checks/manual/video-caption-manual.js +15 -4
  141. package/src/checks/manual-review.js +56 -12
  142. package/src/core/aria-helpers.js +1127 -886
  143. package/src/core/contrast-helpers.js +1217 -1062
  144. package/src/core/dom-helpers.js +4175 -3917
  145. package/src/core/dom-runner.js +720 -604
  146. package/src/core/frame-messaging.js +189 -138
  147. package/src/core/frame-scan.js +94 -82
  148. package/src/core/rollup-composites.js +94 -102
  149. package/src/core/rule-meta.js +58 -41
  150. package/src/core.js +36552 -29111
  151. package/src/i18n/en.js +1194 -889
  152. package/src/i18n/fr.js +1136 -795
  153. package/src/policy/contracts.js +13 -13
  154. package/src/policy/resolvePolicy.js +48 -44
  155. package/src/report.js +63 -43
  156. package/src/sarif.js +175 -0
  157. package/surea11y.browser.js +36042 -0
@@ -11,184 +11,205 @@
11
11
  * Human review is required to confirm that the provided text alternative is accurate and appropriate.
12
12
  */
13
13
 
14
- const id = "object-text-alternative-quality";
14
+ const id = 'object-text-alternative-quality';
15
15
 
16
16
  const meta = {
17
- title: "<object> text alternative must be appropriate (manual review)",
18
- description: "Flags <object> elements with detected fallback or name for human review of equivalence and appropriateness.",
19
- i18n: {
20
- titleKey: "object_textAltQuality_title",
21
- descriptionKey: "object_textAltQuality_description"
22
- },
23
- helpUrl: null,
24
- tags: ["wcag2a", "wcag111", "nontext", "object", "manual", "atomic"],
25
- wcagSc: ['1.1.1'],
26
- normativeMappings: [
27
- {standard: 'WCAG', version: '2.2', requirement: '1.1.1', title: 'Non-text Content', conformanceLevel: 'A'}
28
- ],
29
- defaultSeverity: 'minor',
30
- category: 'perceivable',
31
- type: 'manual',
32
- defaultConfidence: 'medium',
33
- coverage: {
34
- facetsBySc: {
35
- '1.1.1': ['text-alternative-quality']
36
- }
17
+ title: '<object> text alternative must be appropriate (manual review)',
18
+ description:
19
+ 'Flags <object> elements with detected fallback or name for human review of equivalence and appropriateness.',
20
+ i18n: {
21
+ titleKey: 'object_textAltQuality_title',
22
+ descriptionKey: 'object_textAltQuality_description'
23
+ },
24
+ helpUrl: null,
25
+ tags: ['wcag2a', 'wcag111', 'nontext', 'object', 'manual', 'atomic'],
26
+ wcagSc: ['1.1.1'],
27
+ normativeMappings: [
28
+ {
29
+ standard: 'WCAG',
30
+ version: '2.2',
31
+ requirement: '1.1.1',
32
+ title: 'Non-text Content',
33
+ conformanceLevel: 'A'
34
+ }
35
+ ],
36
+ defaultSeverity: 'minor',
37
+ category: 'perceivable',
38
+ type: 'manual',
39
+ defaultConfidence: 'medium',
40
+ coverage: {
41
+ facetsBySc: {
42
+ '1.1.1': ['text-alternative-quality']
37
43
  }
44
+ }
38
45
  };
39
46
 
40
47
  function runInPage(ctx) {
41
-
42
- const {document, root, helpers, rule} = ctx;
43
- const safeRoot = root || document;
44
-
45
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
46
- const queryAll = helpers && typeof helpers.queryAll === 'function'
47
- ? helpers.queryAll
48
- : (sel) => {
49
- try {
50
- return safeRoot && safeRoot.querySelectorAll ? Array.from(safeRoot.querySelectorAll(sel)) : [];
51
- } catch {
52
- return [];
53
- }
48
+ const { document, root, helpers, rule } = ctx;
49
+ const safeRoot = root || document;
50
+
51
+ const queryAllSmart =
52
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
+ const queryAll =
54
+ helpers && typeof helpers.queryAll === 'function'
55
+ ? helpers.queryAll
56
+ : (sel) => {
57
+ try {
58
+ return safeRoot && safeRoot.querySelectorAll
59
+ ? Array.from(safeRoot.querySelectorAll(sel))
60
+ : [];
61
+ } catch {
62
+ return [];
63
+ }
54
64
  };
55
65
 
56
- const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
57
- ? helpers.getEligibilityInfo
58
- : null;
59
-
60
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function'
61
- ? helpers.isAccTreeEligible
62
- : null;
63
-
64
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function'
65
- ? helpers.getFocusableInfo
66
- : null;
67
-
68
- function isRolePresentationExcluded(el) {
69
- const role = (() => {
70
- try {
71
- return String(el.getAttribute('role') || '').trim().toLowerCase();
72
- } catch {
73
- return '';
74
- }
75
- })();
76
- if (role !== 'presentation' && role !== 'none') return false;
77
-
78
- // Exclude only when NOT focusable (mirrors img-alt-present policy)
79
- let focusable = false;
80
- if (getFocusableInfo) {
81
- const fi = (() => {
82
- try {
83
- return getFocusableInfo(el, ctx);
84
- } catch {
85
- return null;
86
- }
87
- })();
88
- focusable = !!(fi && fi.focusable);
89
- } else {
90
- const tabindex = el.getAttribute('tabindex');
91
- focusable = tabindex != null && String(tabindex).trim() !== '' && !Number.isNaN(Number(String(tabindex).trim()));
92
- }
93
- return !focusable;
94
- }
66
+ const getEligibilityInfo =
67
+ helpers && typeof helpers.getEligibilityInfo === 'function' ? helpers.getEligibilityInfo : null;
68
+
69
+ const isAccTreeEligible =
70
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
95
71
 
72
+ const getFocusableInfo =
73
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
96
74
 
97
- const els = (() => {
75
+ function isRolePresentationExcluded(el) {
76
+ const role = (() => {
77
+ try {
78
+ return String(el.getAttribute('role') || '')
79
+ .trim()
80
+ .toLowerCase();
81
+ } catch {
82
+ return '';
83
+ }
84
+ })();
85
+ if (role !== 'presentation' && role !== 'none') return false;
86
+
87
+ // Exclude only when NOT focusable (mirrors img-alt-present policy)
88
+ let focusable;
89
+ if (getFocusableInfo) {
90
+ const fi = (() => {
98
91
  try {
99
- return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []);
92
+ return getFocusableInfo(el, ctx);
100
93
  } catch {
101
- return queryAll('object');
94
+ return null;
102
95
  }
103
- })();
104
-
105
- if (!els.length) {
106
- return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
96
+ })();
97
+ focusable = !!(fi && fi.focusable);
98
+ } else {
99
+ const tabindex = el.getAttribute('tabindex');
100
+ focusable =
101
+ tabindex != null &&
102
+ String(tabindex).trim() !== '' &&
103
+ !Number.isNaN(Number(String(tabindex).trim()));
107
104
  }
105
+ return !focusable;
106
+ }
107
+
108
+ const els = (() => {
109
+ try {
110
+ return Array.from((queryAllSmart ? queryAllSmart('object') : queryAll('object')) || []);
111
+ } catch {
112
+ return queryAll('object');
113
+ }
114
+ })();
108
115
 
109
- const occurrences = [];
110
- let applicableCount = 0;
111
-
112
- for (const el of els) {
113
- if (!el || !el.getAttribute) continue;
114
-
115
- if (isAccTreeEligible) {
116
- const elig = (() => {
117
- try {
118
- return isAccTreeEligible(el, ctx);
119
- } catch {
120
- return {eligible: true, reasons: []};
121
- }
122
- })();
123
- if (elig && elig.eligible === false) continue;
124
- }
125
-
126
- if (isRolePresentationExcluded(el)) continue;
116
+ if (!els.length) {
117
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
118
+ }
127
119
 
128
- const trim = (v) => (v == null ? '' : String(v)).trim();
120
+ const occurrences = [];
121
+ let applicableCount = 0;
129
122
 
130
- let fallbackText = '';
131
- let ariaLabel = '';
132
- let ariaLabelledBy = '';
133
- let title = '';
134
- let labelledByText = '';
123
+ for (const el of els) {
124
+ if (!el || !el.getAttribute) continue;
135
125
 
126
+ if (isAccTreeEligible) {
127
+ const elig = (() => {
136
128
  try {
137
- fallbackText = trim(el.textContent || '');
138
- ariaLabel = trim(el.getAttribute('aria-label'));
139
- ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
140
- title = trim(el.getAttribute('title'));
141
- } catch {}
142
-
143
- // Only resolve idrefs if needed/present
144
- if (!ariaLabel && ariaLabelledBy && helpers && typeof helpers.getTextFromIdRefs === 'function') {
145
- try {
146
- const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
147
- labelledByText = trim(t && t.text);
148
- } catch {}
129
+ return isAccTreeEligible(el, ctx);
130
+ } catch {
131
+ return { eligible: true, reasons: [] };
149
132
  }
133
+ })();
134
+ if (elig && elig.eligible === false) continue;
135
+ }
150
136
 
151
- const details = {
152
- fallbackText: fallbackText || null,
153
- ariaLabel: ariaLabel || null,
154
- ariaLabelledBy: ariaLabelledBy || null,
155
- ariaLabelledByText: labelledByText || null,
156
- title: title || null
157
- };
158
-
159
- const hasMechanism = !!(details.fallbackText || details.ariaLabel || details.ariaLabelledByText || details.title);
160
- if (!hasMechanism) continue;
161
-
162
- applicableCount += 1;
163
-
164
- const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
165
-
166
- const baseOccurrence = {
167
- summary: 'Review text alternative for <object> for equivalence and appropriateness.',
168
- hint: 'Confirm the fallback content or ARIA name provides an equivalent alternative for the embedded content.',
169
- i18n: {
170
- summaryKey: 'object_textAltQuality_summary_cantTell',
171
- hintKey: 'object_textAltQuality_hint_cantTell',
172
- params: { element: 'object' }
173
- },
174
- data: {
175
- visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
176
- details
177
- }
178
- };
179
-
180
- if (helpers && typeof helpers.reportOccurrence === 'function') {
181
- occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
182
- } else {
183
- occurrences.push({ selector: '', html: '', ...baseOccurrence });
184
- }
137
+ if (isRolePresentationExcluded(el)) continue;
138
+
139
+ const trim = (v) => (v == null ? '' : String(v)).trim();
140
+
141
+ let fallbackText = '';
142
+ let ariaLabel = '';
143
+ let ariaLabelledBy = '';
144
+ let title = '';
145
+ let labelledByText = '';
146
+
147
+ try {
148
+ fallbackText = trim(el.textContent || '');
149
+ ariaLabel = trim(el.getAttribute('aria-label'));
150
+ ariaLabelledBy = trim(el.getAttribute('aria-labelledby'));
151
+ title = trim(el.getAttribute('title'));
152
+ } catch {}
153
+
154
+ // Only resolve idrefs if needed/present
155
+ if (
156
+ !ariaLabel &&
157
+ ariaLabelledBy &&
158
+ helpers &&
159
+ typeof helpers.getTextFromIdRefs === 'function'
160
+ ) {
161
+ try {
162
+ const t = helpers.getTextFromIdRefs(ariaLabelledBy, ctx);
163
+ labelledByText = trim(t && t.text);
164
+ } catch {}
185
165
  }
186
166
 
187
- if (applicableCount === 0) {
188
- return {ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: []};
167
+ const details = {
168
+ fallbackText: fallbackText || null,
169
+ ariaLabel: ariaLabel || null,
170
+ ariaLabelledBy: ariaLabelledBy || null,
171
+ ariaLabelledByText: labelledByText || null,
172
+ title: title || null
173
+ };
174
+
175
+ const hasMechanism = !!(
176
+ details.fallbackText ||
177
+ details.ariaLabel ||
178
+ details.ariaLabelledByText ||
179
+ details.title
180
+ );
181
+ if (!hasMechanism) continue;
182
+
183
+ applicableCount += 1;
184
+
185
+ const eligInfo = getEligibilityInfo ? getEligibilityInfo(el, ctx, { targetSet: 'acc' }) : null;
186
+
187
+ const baseOccurrence = {
188
+ summary: 'Review text alternative for <object> for equivalence and appropriateness.',
189
+ hint: 'Confirm the fallback content or ARIA name provides an equivalent alternative for the embedded content.',
190
+ i18n: {
191
+ summaryKey: 'object_textAltQuality_summary_cantTell',
192
+ hintKey: 'object_textAltQuality_hint_cantTell',
193
+ params: { element: 'object' }
194
+ },
195
+ data: {
196
+ visibilityFilter: eligInfo || { targetSet: 'acc', accEligible: null, reasons: [] },
197
+ details
198
+ }
199
+ };
200
+
201
+ if (helpers && typeof helpers.reportOccurrence === 'function') {
202
+ occurrences.push(helpers.reportOccurrence(el, baseOccurrence));
203
+ } else {
204
+ occurrences.push({ selector: '', html: '', ...baseOccurrence });
189
205
  }
206
+ }
207
+
208
+ if (applicableCount === 0) {
209
+ return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
210
+ }
190
211
 
191
- return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
212
+ return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
192
213
  }
193
214
 
194
- module.exports = {id, meta, runInPage};
215
+ module.exports = { id, meta, runInPage };
@@ -34,7 +34,8 @@ const id = 'p-as-heading';
34
34
 
35
35
  const meta = {
36
36
  title: 'A <p> styled to look like a heading should probably be a real heading',
37
- description: 'Flags short <p> elements whose entire text is bold and rendered at >=18px, for manual review of whether a real heading element should be used instead.',
37
+ description:
38
+ 'Flags short <p> elements whose entire text is bold and rendered at >=18px, for manual review of whether a real heading element should be used instead.',
38
39
  i18n: {
39
40
  titleKey: 'pAsHeading_title',
40
41
  descriptionKey: 'pAsHeading_description'
@@ -43,7 +44,13 @@ const meta = {
43
44
  tags: ['wcag2a', 'wcag131', 'structure', 'atomic', 'manual'],
44
45
  wcagSc: ['1.3.1'],
45
46
  normativeMappings: [
46
- { standard: 'WCAG', version: '2.2', requirement: '1.3.1', title: 'Info and Relationships', conformanceLevel: 'A' }
47
+ {
48
+ standard: 'WCAG',
49
+ version: '2.2',
50
+ requirement: '1.3.1',
51
+ title: 'Info and Relationships',
52
+ conformanceLevel: 'A'
53
+ }
47
54
  ],
48
55
  defaultSeverity: 'minor',
49
56
  category: 'perceivable',
@@ -53,12 +60,14 @@ 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
  const MAX_HEADING_LIKE_CHARS = 120;
59
66
  const MIN_FONT_SIZE_PX = 18;
60
67
 
61
- function trim(v) { return (v == null ? '' : String(v)).trim(); }
68
+ function trim(v) {
69
+ return (v == null ? '' : String(v)).trim();
70
+ }
62
71
 
63
72
  function safeComputedStyle(el) {
64
73
  try {
@@ -67,7 +76,8 @@ function runInPage(ctx) {
67
76
  const cs = helpers.computedStyle(el);
68
77
  if (cs) return cs;
69
78
  }
70
- const view = (el.ownerDocument && el.ownerDocument.defaultView) ? el.ownerDocument.defaultView : null;
79
+ const view =
80
+ el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : null;
71
81
  if (view && typeof view.getComputedStyle === 'function') return view.getComputedStyle(el);
72
82
  } catch {}
73
83
  return null;
@@ -124,7 +134,7 @@ function runInPage(ctx) {
124
134
  if (fontSizePx < MIN_FONT_SIZE_PX) continue;
125
135
 
126
136
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
127
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
137
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : el.outerHTML || '';
128
138
 
129
139
  const baseOccurrence = {
130
140
  selector: stableSelector,
@@ -153,7 +163,12 @@ function runInPage(ctx) {
153
163
  }
154
164
 
155
165
  if (occurrences.length) {
156
- return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
166
+ return {
167
+ ruleId: rule.ruleId,
168
+ outcome: 'cantTell',
169
+ severity: rule.defaultSeverity || 'minor',
170
+ occurrences
171
+ };
157
172
  }
158
173
 
159
174
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -25,7 +25,7 @@
25
25
  * page (CDC's flu page, 2026-07-30): its only `<h1>` sits inside a
26
26
  * `display:none` ancestor — genuinely unreachable by sighted and screen
27
27
  * reader users alike — and a raw `document.querySelectorAll` credited it
28
- * anyway, reporting `notApplicable` where axe-core correctly fails. This
28
+ * anyway, incorrectly reporting `notApplicable`. This
29
29
  * does NOT regress purely-visually-clipped-but-AT-exposed headings (e.g.
30
30
  * eBay's homepage `<h1>` hidden via clip-path/off-screen positioning,
31
31
  * `visibility:visible`, no `aria-hidden`) — `isAccTreeEligible` only
@@ -37,7 +37,8 @@ const id = 'page-has-heading-one';
37
37
 
38
38
  const meta = {
39
39
  title: 'Page should have a level-one heading',
40
- description: 'Checks that the page has at least one level-one heading (<h1> or role="heading" with aria-level="1").',
40
+ description:
41
+ 'Checks that the page has at least one level-one heading (<h1> or role="heading" with aria-level="1").',
41
42
  i18n: {
42
43
  titleKey: 'pageHasHeadingOne_title',
43
44
  descriptionKey: 'pageHasHeadingOne_description'
@@ -70,7 +71,9 @@ function runInPage(ctx) {
70
71
  }
71
72
 
72
73
  function normalizeWs(s) {
73
- return String(s || '').replace(/\s+/g, ' ').trim();
74
+ return String(s || '')
75
+ .replace(/\s+/g, ' ')
76
+ .trim();
74
77
  }
75
78
 
76
79
  function getExplicitRoleToken(el) {
@@ -90,7 +93,8 @@ function runInPage(ctx) {
90
93
  return tag === 'h1';
91
94
  }
92
95
 
93
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
96
+ const isAccTreeEligible =
97
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
94
98
 
95
99
  function isExposedToAt(el) {
96
100
  if (!isAccTreeEligible) return true;
@@ -105,11 +109,12 @@ function runInPage(ctx) {
105
109
 
106
110
  // queryAllSmart (shadow-DOM-aware) instead of plain document.querySelectorAll -- see
107
111
  // landmark-one-main-manual.js's identical precedent.
108
- let nodes = [];
112
+ let nodes;
109
113
  try {
110
- nodes = helpers && typeof helpers.queryAllSmart === 'function'
111
- ? helpers.queryAllSmart('h1, [role]')
112
- : document.querySelectorAll('h1, [role]');
114
+ nodes =
115
+ helpers && typeof helpers.queryAllSmart === 'function'
116
+ ? helpers.queryAllSmart('h1, [role]')
117
+ : document.querySelectorAll('h1, [role]');
113
118
  } catch {
114
119
  nodes = [];
115
120
  }
@@ -121,26 +126,30 @@ function runInPage(ctx) {
121
126
  }
122
127
 
123
128
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
124
- const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(body) : (body.outerHTML || '').slice(0, 200);
129
+ const html = helpers.getOuterHtmlSnippet
130
+ ? helpers.getOuterHtmlSnippet(body)
131
+ : (body.outerHTML || '').slice(0, 200);
125
132
 
126
133
  return {
127
134
  ruleId: rule.ruleId,
128
135
  outcome: 'cantTell',
129
136
  severity: rule.defaultSeverity || 'minor',
130
- occurrences: [{
131
- selector: stableSelector,
132
- html,
133
- summary: 'This page has no level-one heading.',
134
- hint: 'Add a level-one heading (<h1> or role="heading" aria-level="1") that identifies the page\'s main content.',
135
- i18n: {
136
- summaryKey: 'pageHasHeadingOne_summary_cantTell',
137
- hintKey: 'pageHasHeadingOne_hint_cantTell',
138
- params: {}
139
- },
140
- data: {
141
- details: { reasonCode: 'HEADING_ONE_MISSING' }
137
+ occurrences: [
138
+ {
139
+ selector: stableSelector,
140
+ html,
141
+ summary: 'This page has no level-one heading.',
142
+ hint: 'Add a level-one heading (<h1> or role="heading" aria-level="1") that identifies the page\'s main content.',
143
+ i18n: {
144
+ summaryKey: 'pageHasHeadingOne_summary_cantTell',
145
+ hintKey: 'pageHasHeadingOne_hint_cantTell',
146
+ params: {}
147
+ },
148
+ data: {
149
+ details: { reasonCode: 'HEADING_ONE_MISSING' }
150
+ }
142
151
  }
143
- }]
152
+ ]
144
153
  };
145
154
  }
146
155