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