@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
@@ -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'
@@ -53,6 +54,14 @@ const meta = {
53
54
  coverage: {}
54
55
  };
55
56
 
57
+ // This check is inherently whole-document (does the PAGE have this
58
+ // property?), not evaluable per-subtree -- notApplicable when contextSelector
59
+ // scoped this run narrower than the whole document, or when
60
+ // engineOptions.fragment:true was set (see helpers.isWholeDocumentScope).
61
+ function applicability(ctx) {
62
+ return ctx.helpers.isWholeDocumentScope ? ctx.helpers.isWholeDocumentScope() : true;
63
+ }
64
+
56
65
  function runInPage(ctx) {
57
66
  const { document, helpers, rule } = ctx;
58
67
 
@@ -62,7 +71,9 @@ function runInPage(ctx) {
62
71
  }
63
72
 
64
73
  function normalizeWs(s) {
65
- return String(s || '').replace(/\s+/g, ' ').trim();
74
+ return String(s || '')
75
+ .replace(/\s+/g, ' ')
76
+ .trim();
66
77
  }
67
78
 
68
79
  function getExplicitRoleToken(el) {
@@ -82,7 +93,8 @@ function runInPage(ctx) {
82
93
  return tag === 'h1';
83
94
  }
84
95
 
85
- const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
96
+ const isAccTreeEligible =
97
+ helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
86
98
 
87
99
  function isExposedToAt(el) {
88
100
  if (!isAccTreeEligible) return true;
@@ -97,11 +109,12 @@ function runInPage(ctx) {
97
109
 
98
110
  // queryAllSmart (shadow-DOM-aware) instead of plain document.querySelectorAll -- see
99
111
  // landmark-one-main-manual.js's identical precedent.
100
- let nodes = [];
112
+ let nodes;
101
113
  try {
102
- nodes = helpers && typeof helpers.queryAllSmart === 'function'
103
- ? helpers.queryAllSmart('h1, [role]')
104
- : document.querySelectorAll('h1, [role]');
114
+ nodes =
115
+ helpers && typeof helpers.queryAllSmart === 'function'
116
+ ? helpers.queryAllSmart('h1, [role]')
117
+ : document.querySelectorAll('h1, [role]');
105
118
  } catch {
106
119
  nodes = [];
107
120
  }
@@ -113,27 +126,31 @@ function runInPage(ctx) {
113
126
  }
114
127
 
115
128
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(body) : 'body';
116
- 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);
117
132
 
118
133
  return {
119
134
  ruleId: rule.ruleId,
120
135
  outcome: 'cantTell',
121
136
  severity: rule.defaultSeverity || 'minor',
122
- occurrences: [{
123
- selector: stableSelector,
124
- html,
125
- summary: 'This page has no level-one heading.',
126
- hint: 'Add a level-one heading (<h1> or role="heading" aria-level="1") that identifies the page\'s main content.',
127
- i18n: {
128
- summaryKey: 'pageHasHeadingOne_summary_cantTell',
129
- hintKey: 'pageHasHeadingOne_hint_cantTell',
130
- params: {}
131
- },
132
- data: {
133
- 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
+ }
134
151
  }
135
- }]
152
+ ]
136
153
  };
137
154
  }
138
155
 
139
- module.exports = { id, meta, runInPage };
156
+ module.exports = { id, meta, runInPage, applicability };